sdk 0.37.2 update for release #547
89
app/package-lock.json
generated
|
@ -895,9 +895,9 @@
|
|||
}
|
||||
},
|
||||
"@react-navigation/core": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.4.1.tgz",
|
||||
"integrity": "sha512-slslu4FmjKQMO/EKGGqqGsfC6evQLdbJM2ROACcC2Xxf0+nPeZV5ND8HHukUZZucJRE6Bg/NI+zC1XSBYRjhnw==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-3.4.2.tgz",
|
||||
"integrity": "sha512-7G+iDzLSTeOUU4vVZeRZKJ+Bd7ds7ZxYNqZcB8i0KlBeQEQfR74Ounfu/p0KIEq2RiNnaE3QT7WVP3C87sebzw==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"path-to-regexp": "^1.7.0",
|
||||
|
@ -916,12 +916,12 @@
|
|||
}
|
||||
},
|
||||
"@react-navigation/native": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.4.1.tgz",
|
||||
"integrity": "sha512-pMAPQfvwC4DvhQfsrXKAf+FiU+A5XAh216v17rEePSFcbeOEt7cvewmWxCxydN/vFjJChFiPV+xnjJyJBdPLOg==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-3.5.0.tgz",
|
||||
"integrity": "sha512-TmGOis++ejEXG3sqNJhCSKqB0/qLu3FQgDtO959qpqif36R/diR8SQwJqeSdofoEiK3CepdhFlTCeHdS1/+MsQ==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^3.0.1",
|
||||
"react-native-safe-area-view": "^0.13.0",
|
||||
"react-native-safe-area-view": "^0.14.1",
|
||||
"react-native-screens": "^1.0.0 || ^1.0.0-alpha"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -934,9 +934,9 @@
|
|||
}
|
||||
},
|
||||
"react-native-safe-area-view": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.13.1.tgz",
|
||||
"integrity": "sha512-d/pu2866jApSwLtK/xWAvMXZkNTIQcFrjjbcTATBrmIfFNnu8TNFUcMRFpfJ+eOn5nmx7uGmDvs9B53Ft7JGpQ==",
|
||||
"version": "0.14.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-view/-/react-native-safe-area-view-0.14.4.tgz",
|
||||
"integrity": "sha512-ypDQVoAyNHBhMR1IGfadm8kskNzPg5czrDAzQEu5MXG9Ahoi5f1cL/rT2KO+R9f6xRjf6b1IjY53m0B0xHRd0A==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^2.3.1"
|
||||
},
|
||||
|
@ -3350,11 +3350,6 @@
|
|||
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
|
||||
"integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU="
|
||||
},
|
||||
"deep-diff": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
|
||||
"integrity": "sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ="
|
||||
},
|
||||
"define-property": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
|
||||
|
@ -4823,8 +4818,8 @@
|
|||
}
|
||||
},
|
||||
"lbry-redux": {
|
||||
"version": "github:lbryio/lbry-redux#4b3769fc2dcc4c93771aa4c5dbb64d0e97f6375f",
|
||||
"from": "github:lbryio/lbry-redux",
|
||||
"version": "github:lbryio/lbry-redux#5cff70a26b05b40f2693bbae6bf50100a6781e50",
|
||||
"from": "github:lbryio/lbry-redux#purchase-uri-failures",
|
||||
"requires": {
|
||||
"proxy-polyfill": "0.1.6",
|
||||
"reselect": "^3.0.0",
|
||||
|
@ -4876,6 +4871,11 @@
|
|||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.11.tgz",
|
||||
"integrity": "sha512-DHb1ub+rMjjrxqlB3H56/6MXtm1lSksDp2rA2cNWjG8mlDUYFhUj3Di2Zn5IwSU87xLv8tNIQ7sSwE/YOX/D/Q=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
|
||||
},
|
||||
"lodash.forin": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.forin/-/lodash.forin-4.4.0.tgz",
|
||||
|
@ -5467,9 +5467,9 @@
|
|||
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
|
||||
},
|
||||
"nan": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz",
|
||||
"integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==",
|
||||
"version": "2.14.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
|
||||
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
|
||||
"optional": true
|
||||
},
|
||||
"nanomatch": {
|
||||
|
@ -6356,9 +6356,9 @@
|
|||
}
|
||||
},
|
||||
"react-native-tab-view": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.3.4.tgz",
|
||||
"integrity": "sha512-iufNROTPr4+Z/IKijlp5faEANiDBWxhpgx9NSCg3esZ+HN5+UtFwB0xkn4XpNRqCvbzeBkgKMRJL3V6kr5NhWg==",
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-tab-view/-/react-native-tab-view-1.4.1.tgz",
|
||||
"integrity": "sha512-Bke8KkDcDhvB/z0AS7MnQKMD2p6Kwfc1rSKlMOvg9CC5CnClQ2QEnhPSbwegKDYhUkBI92iH/BYy7hNSm5kbUQ==",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.1"
|
||||
}
|
||||
|
@ -6606,15 +6606,15 @@
|
|||
}
|
||||
},
|
||||
"react-navigation": {
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-3.9.1.tgz",
|
||||
"integrity": "sha512-4rUQXGT0yvLb9yX9NDuKdrXb/NcAPGUHDTlto8Fg4Tm23uuyBBSrDVStqC59rUM4JcoQnRqhenN2wXGvWE+WYA==",
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation/-/react-navigation-3.11.0.tgz",
|
||||
"integrity": "sha512-wlPcDtNiIdPeYxNQ/MN4arY5Xe9EphD2QVpRuvvuPWW+BamF3AJaIy060r3Yz59DODAoWllscabat/yqnih8Tg==",
|
||||
"requires": {
|
||||
"@react-navigation/core": "~3.4.1",
|
||||
"@react-navigation/native": "~3.4.0",
|
||||
"react-navigation-drawer": "1.2.1",
|
||||
"react-navigation-stack": "1.3.0",
|
||||
"react-navigation-tabs": "1.1.2"
|
||||
"@react-navigation/native": "~3.5.0",
|
||||
"react-navigation-drawer": "~1.2.1",
|
||||
"react-navigation-stack": "~1.4.0",
|
||||
"react-navigation-tabs": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"react-navigation-drawer": {
|
||||
|
@ -6634,19 +6634,19 @@
|
|||
}
|
||||
},
|
||||
"react-navigation-stack": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation-stack/-/react-navigation-stack-1.3.0.tgz",
|
||||
"integrity": "sha512-ouyD1GkRksJSGuvAuqrJnlJnZ5g2g/+/WB/MTa8BzjSBvyOgruD5TrmEkpViCOMr1R17C8D4Htln90H4D+NV3Q=="
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation-stack/-/react-navigation-stack-1.4.0.tgz",
|
||||
"integrity": "sha512-zEe9wCA0Ot8agarYb//0nSWYW1GM+1R0tY/nydUV0EizeJ27At0EklYVWvYEuYU6C48va6cu8OPL7QD/CcJACw=="
|
||||
},
|
||||
"react-navigation-tabs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation-tabs/-/react-navigation-tabs-1.1.2.tgz",
|
||||
"integrity": "sha512-D4fecSwZfvNh5WHTURmUVrNSgy3tiNfID0n5eKTOhCz4Sls4EM2l27UTX833ngxXhQ1FqRtBxzQZ+Dp1FWJ1pw==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/react-navigation-tabs/-/react-navigation-tabs-1.1.4.tgz",
|
||||
"integrity": "sha512-py2hLCRxPwXOzmY1W9XcY1rWXxdK6RGW/aXh56G9gIf8cpHNDhy/bJV4e46/JrVcse3ybFaN0liT09/DM/NdwQ==",
|
||||
"requires": {
|
||||
"hoist-non-react-statics": "^2.5.0",
|
||||
"prop-types": "^15.6.1",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"react-native-tab-view": "^1.3.4"
|
||||
"react-native-tab-view": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"react-proxy": {
|
||||
|
@ -6735,14 +6735,6 @@
|
|||
"symbol-observable": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"redux-logger": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
|
||||
"integrity": "sha1-91VZZvMJjzyIYExEnPC69XeCdL8=",
|
||||
"requires": {
|
||||
"deep-diff": "^0.3.5"
|
||||
}
|
||||
},
|
||||
"redux-persist": {
|
||||
"version": "4.10.2",
|
||||
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.10.2.tgz",
|
||||
|
@ -6771,10 +6763,11 @@
|
|||
}
|
||||
},
|
||||
"redux-persist-transform-filter": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/redux-persist-transform-filter/-/redux-persist-transform-filter-0.0.10.tgz",
|
||||
"integrity": "sha1-mjsQbOiTnSy79SEsdH7Rd//xQoA=",
|
||||
"version": "0.0.18",
|
||||
"resolved": "https://registry.npmjs.org/redux-persist-transform-filter/-/redux-persist-transform-filter-0.0.18.tgz",
|
||||
"integrity": "sha512-x9NxuHNDnK/THLLBqwP1tqw0yIcuxuVYXBssgGcmm5anxL0flbpLQGB5CbFYHWGG68VdQKr1vUneVnttxWJDtA==",
|
||||
"requires": {
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"lodash.forin": "^4.4.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.isempty": "^4.4.0",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"dependencies": {
|
||||
"base-64": "^0.1.0",
|
||||
"@expo/vector-icons": "^8.1.0",
|
||||
"lbry-redux": "lbryio/lbry-redux",
|
||||
"lbry-redux": "lbryio/lbry-redux#purchase-uri-failures",
|
||||
"lbryinc": "lbryio/lbryinc#check-sync",
|
||||
"lodash": ">=4.17.11",
|
||||
"merge": ">=1.2.1",
|
||||
|
@ -25,16 +25,15 @@
|
|||
"react-native-phone-input": "lbryio/react-native-phone-input",
|
||||
"react-native-vector-icons": "^6.4.2",
|
||||
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
|
||||
"react-navigation": "^3.6.1",
|
||||
"react-navigation": "^3.11.0",
|
||||
"react-navigation-redux-helpers": "^3.0.0",
|
||||
"react-redux": "^5.0.3",
|
||||
"redux": "^3.6.0",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-persist": "^4.8.0",
|
||||
"redux-persist": "^4.10.2",
|
||||
"redux-persist-filesystem-storage": "^1.3.2",
|
||||
"redux-persist-transform-compress": "^4.2.0",
|
||||
"redux-persist-transform-filter": "0.0.10",
|
||||
"redux-thunk": "^2.2.0",
|
||||
"redux-persist-transform-filter": "0.0.18",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"rn-fetch-blob": "^0.10.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -34,7 +34,6 @@ import {
|
|||
TextInput,
|
||||
ToastAndroid
|
||||
} from 'react-native';
|
||||
import { doDeleteCompleteBlobs } from 'redux/actions/file';
|
||||
import { selectDrawerStack } from 'redux/selectors/drawer';
|
||||
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
|
||||
import {
|
||||
|
@ -116,7 +115,7 @@ const myLbryStack = createStackNavigator({
|
|||
Downloads: {
|
||||
screen: DownloadsPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
title: 'Downloads',
|
||||
title: 'Library',
|
||||
header: null
|
||||
})
|
||||
}
|
||||
|
@ -179,7 +178,7 @@ const drawer = createDrawerNavigator({
|
|||
drawerIcon: ({ tintColor }) => <Icon name="award" size={20} style={{ color: tintColor }} />
|
||||
}},
|
||||
MyLBRYStack: { screen: myLbryStack, navigationOptions: {
|
||||
title: 'Downloads', drawerIcon: ({ tintColor }) => <Icon name="folder" size={20} style={{ color: tintColor }} />
|
||||
title: 'Library', drawerIcon: ({ tintColor }) => <Icon name="download" size={20} style={{ color: tintColor }} />
|
||||
}},
|
||||
Settings: { screen: SettingsPage, navigationOptions: {
|
||||
drawerLockMode: 'locked-closed',
|
||||
|
@ -225,7 +224,6 @@ const mainStackNavigator = new createStackNavigator({
|
|||
});
|
||||
|
||||
|
||||
|
||||
export const AppNavigator = mainStackNavigator;
|
||||
export const reactNavigationMiddleware = createReactNavigationReduxMiddleware(
|
||||
state => state.nav,
|
||||
|
@ -299,9 +297,11 @@ class AppWithNavigationState extends React.Component {
|
|||
|
||||
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
|
||||
|
||||
// upon successful email verification, check wallet sync
|
||||
// upon successful email verification, do wallet sync (if password has been set)
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
dispatch(doGetSync(walletPassword));
|
||||
if (walletPassword && walletPassword.trim().length > 0) {
|
||||
dispatch(doGetSync(walletPassword));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -343,6 +343,7 @@ class AppWithNavigationState extends React.Component {
|
|||
if (!emailVerifyErrorMessage) {
|
||||
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_EMAIL);
|
||||
}
|
||||
|
||||
AsyncStorage.removeItem(Constants.KEY_SHOULD_VERIFY_EMAIL);
|
||||
dispatch(doToast({ message }));
|
||||
}
|
||||
|
@ -370,8 +371,6 @@ class AppWithNavigationState extends React.Component {
|
|||
}
|
||||
|
||||
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||
// Cleanup blobs for completed files upon app resume to save space
|
||||
dispatch(doDeleteCompleteBlobs());
|
||||
if (backgroundPlayEnabled || NativeModules.BackgroundMedia) {
|
||||
NativeModules.BackgroundMedia.hidePlaybackNotification();
|
||||
}
|
||||
|
|
|
@ -13,6 +13,9 @@ class CategoryList extends React.PureComponent {
|
|||
<FlatList
|
||||
style={discoverStyle.horizontalScrollContainer}
|
||||
contentContainerStyle={discoverStyle.horizontalScrollPadding}
|
||||
initialNumToRender={3}
|
||||
maxToRenderPerBatch={3}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={ ({item}) => (
|
||||
<FileItem
|
||||
style={discoverStyle.fileItem}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doPurchaseUri,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectDownloadingForUri,
|
||||
makeSelectLoadingForUri,
|
||||
} from 'lbry-redux';
|
||||
import { doFetchCostInfoForUri, makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { doPurchaseUri, doStartDownload } from 'redux/actions/file';
|
||||
import { doStartDownload } from 'redux/actions/file';
|
||||
import FileDownloadButton from './view';
|
||||
|
||||
const select = (state, props) => ({
|
||||
|
@ -16,7 +17,7 @@ const select = (state, props) => ({
|
|||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
purchaseUri: (uri, failureCallback) => dispatch(doPurchaseUri(uri, null, failureCallback)),
|
||||
purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
|
||||
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
|
||||
import Button from '../button';
|
||||
import fileDownloadButtonStyle from '../../styles/fileDownloadButton';
|
||||
import fileDownloadButtonStyle from 'styles/fileDownloadButton';
|
||||
|
||||
class FileDownloadButton extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
|
@ -13,7 +13,7 @@ class FileDownloadButton extends React.PureComponent {
|
|||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
//this.checkAvailability(nextProps.uri);
|
||||
this.restartDownload(nextProps);
|
||||
//this.restartDownload(nextProps);
|
||||
}
|
||||
|
||||
restartDownload(props) {
|
||||
|
@ -46,7 +46,6 @@ class FileDownloadButton extends React.PureComponent {
|
|||
style,
|
||||
openFile,
|
||||
onButtonLayout,
|
||||
onStartDownloadFailed
|
||||
} = this.props;
|
||||
|
||||
if ((fileInfo && !fileInfo.stopped) || loading || downloading) {
|
||||
|
@ -60,7 +59,7 @@ class FileDownloadButton extends React.PureComponent {
|
|||
<Text style={fileDownloadButtonStyle.text}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (fileInfo === null && !downloading) {
|
||||
} else if (!fileInfo && !downloading) {
|
||||
if (!costInfo) {
|
||||
return (
|
||||
<View style={[style, fileDownloadButtonStyle.container]}>
|
||||
|
@ -76,7 +75,10 @@ class FileDownloadButton extends React.PureComponent {
|
|||
if (NativeModules.Firebase) {
|
||||
NativeModules.Firebase.track('purchase_uri', { uri: uri });
|
||||
}
|
||||
purchaseUri(uri, onStartDownloadFailed);
|
||||
purchaseUri(uri, costInfo, !isPlayable);
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.checkDownloads();
|
||||
}
|
||||
if (isPlayable && onPlay) {
|
||||
this.props.onPlay();
|
||||
}
|
||||
|
|
|
@ -78,8 +78,10 @@ class FileItem extends React.PureComponent {
|
|||
isResolvingUri={isResolvingUri}
|
||||
style={mediaStyle} />
|
||||
|
||||
{(!compactView && fileInfo && fileInfo.completed) && <Icon style={discoverStyle.downloadedIcon} solid={true} color={Colors.BrightGreen} name={"folder"} size={16} />}
|
||||
{(!compactView && (!fileInfo || !fileInfo.completed)) && <FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />}
|
||||
{(!compactView && fileInfo && fileInfo.completed && fileInfo.download_path) &&
|
||||
<Icon style={discoverStyle.downloadedIcon} solid={true} color={Colors.NextLbryGreen} name={"folder"} size={16} />}
|
||||
{(!compactView && (!fileInfo || !fileInfo.completed || !fileInfo.download_path)) &&
|
||||
<FilePrice uri={uri} style={discoverStyle.filePriceContainer} textStyle={discoverStyle.filePriceText} />}
|
||||
{!compactView && <View style={isRewardContent ? discoverStyle.rewardTitleContainer : null}>
|
||||
<Text numberOfLines={1} style={[discoverStyle.fileItemName, discoverStyle.rewardTitle]}>{title}</Text>
|
||||
{isRewardContent && <Icon style={discoverStyle.rewardIcon} name="award" size={14} />}
|
||||
|
|
|
@ -87,7 +87,8 @@ class FileListItem extends React.PureComponent {
|
|||
resizeMode="cover"
|
||||
title={(title || name)}
|
||||
thumbnail={thumbnail} />
|
||||
{fileInfo && fileInfo.completed && <Icon style={fileListStyle.downloadedIcon} solid={true} color={Colors.BrightGreen} name={"folder"} size={16} />}
|
||||
{(fileInfo && fileInfo.completed && fileInfo.download_path) &&
|
||||
<Icon style={fileListStyle.downloadedIcon} solid={true} color={Colors.BrightGreen} name={"folder"} size={16} />}
|
||||
<View style={fileListStyle.detailsContainer}>
|
||||
{featuredResult && <Text style={fileListStyle.featuredUri} numberOfLines={1}>{uri}</Text>}
|
||||
|
||||
|
@ -106,7 +107,8 @@ class FileListItem extends React.PureComponent {
|
|||
}} />}
|
||||
|
||||
<View style={fileListStyle.info}>
|
||||
{fileInfo && <Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>}
|
||||
{(fileInfo && !isNaN(fileInfo.written_bytes) && fileInfo.written_bytes > 0) &&
|
||||
<Text style={fileListStyle.infoText}>{this.getStorageForFileInfo(fileInfo)}</Text>}
|
||||
<DateTime style={fileListStyle.publishInfo} textStyle={fileListStyle.infoText} timeAgo uri={uri} />
|
||||
</View>
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ class FilePrice extends React.PureComponent {
|
|||
<CreditAmount
|
||||
style={textStyle}
|
||||
label={false}
|
||||
amount={costInfo.cost}
|
||||
amount={parseFloat(costInfo.cost)}
|
||||
isEstimate={isEstimate}
|
||||
showFree
|
||||
showFullPrice={showFullPrice}>???</CreditAmount>
|
||||
|
|
4
app/src/component/fileRewardsDriver/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import FileRewardsDriver from './view';
|
||||
|
||||
export default connect()(FileRewardsDriver);
|
20
app/src/component/fileRewardsDriver/view.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
import { Text, TouchableOpacity } from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import filePageStyle from 'styles/filePage';
|
||||
|
||||
class FileRewardsDriver extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const { navigation } = this.props;
|
||||
|
||||
return (
|
||||
<TouchableOpacity style={filePageStyle.rewardDriverCard} onPress={() => navigation.navigate('Rewards')}>
|
||||
<Icon name="award" size={16} style={filePageStyle.rewardIcon} />
|
||||
<Text style={filePageStyle.rewardDriverText}>Earn some credits to access this content.</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FileRewardsDriver;
|
|
@ -1,14 +1,18 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS, savePosition } from 'lbry-redux';
|
||||
import { makeSelectClientSetting } from '../../redux/selectors/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { selectIsPlayerVisible } from 'redux/selectors/drawer';
|
||||
import MediaPlayer from './view';
|
||||
|
||||
const select = state => ({
|
||||
backgroundPlayEnabled: makeSelectClientSetting(SETTINGS.BACKGROUND_PLAY_ENABLED)(state),
|
||||
isPlayerVisible: selectIsPlayerVisible(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
savePosition: (claimId, outpoint, position) => dispatch(savePosition(claimId, outpoint, position)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(MediaPlayer);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
AppState,
|
||||
ActivityIndicator,
|
||||
DeviceEventEmitter,
|
||||
NativeModules,
|
||||
PanResponder,
|
||||
|
@ -9,12 +11,15 @@ import {
|
|||
ScrollView,
|
||||
TouchableOpacity
|
||||
} from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import Video from 'react-native-video';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import FileItemMedia from 'component/fileItemMedia';
|
||||
import mediaPlayerStyle from 'styles/mediaPlayer';
|
||||
|
||||
const positionSaveInterval = 10
|
||||
|
||||
class MediaPlayer extends React.PureComponent {
|
||||
static ControlsTimeout = 3000;
|
||||
|
||||
|
@ -31,7 +36,9 @@ class MediaPlayer extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
encodedFilePath: null,
|
||||
buffering: false,
|
||||
backgroundPlayEnabled: false,
|
||||
autoPaused: false,
|
||||
rate: 1,
|
||||
volume: 1,
|
||||
muted: false,
|
||||
|
@ -85,6 +92,7 @@ class MediaPlayer extends React.PureComponent {
|
|||
const { position } = this.props;
|
||||
if (!isNaN(parseFloat(position)) && position > 0) {
|
||||
this.video.seek(position);
|
||||
this.setState({ currentTime: position }, () => this.setSeekerPosition(this.calculateSeekerPosition()));
|
||||
}
|
||||
|
||||
if (this.props.onMediaLoaded) {
|
||||
Every 10 seconds. Saving every second was resulting in some lag / performance issues. Every 10 seconds. Saving every second was resulting in some lag / performance issues.
|
||||
|
@ -93,10 +101,13 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
|
||||
onProgress = (data) => {
|
||||
const { savePosition, fileInfo } = this.props;
|
||||
const { savePosition, claim } = this.props;
|
||||
|
||||
|
||||
this.setState({ currentTime: data.currentTime }, () => savePosition(fileInfo.claim_id, fileInfo.outpoint, data.currentTime));
|
||||
this.setState({ buffering: false, currentTime: data.currentTime });
|
||||
if (data.currentTime > 0 && Math.floor(data.currentTime) % positionSaveInterval === 0) {
|
||||
const { claim_id: claimId, txid, nout } = claim;
|
||||
savePosition(claimId, `${txid}:${nout}`, data.currentTime);
|
||||
}
|
||||
|
||||
if (!this.state.seeking) {
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
|
@ -140,6 +151,11 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
|
||||
togglePlayerControls = () => {
|
||||
const { setPlayerVisible, isPlayerVisible } = this.props;
|
||||
if (!isPlayerVisible) {
|
||||
setPlayerVisible();
|
||||
}
|
||||
|
||||
if (this.state.areControlsVisible) {
|
||||
this.manualHidePlayerControls();
|
||||
} else {
|
||||
|
@ -149,7 +165,14 @@ class MediaPlayer extends React.PureComponent {
|
|||
|
||||
togglePlay = () => {
|
||||
this.showPlayerControls();
|
||||
this.setState({ paused: !this.state.paused });
|
||||
this.setState({ paused: !this.state.paused }, this.handlePausedState);
|
||||
}
|
||||
|
||||
handlePausedState = () => {
|
||||
if (!this.state.paused) {
|
||||
// onProgress will automatically clear this, so it's fine
|
||||
this.setState({ buffering: true });
|
||||
}
|
||||
}
|
||||
|
||||
toggleFullscreenMode = () => {
|
||||
|
@ -217,7 +240,7 @@ class MediaPlayer extends React.PureComponent {
|
|||
onPanResponderRelease: (evt, gestureState) => {
|
||||
const time = this.getCurrentTimeForSeekerPosition();
|
||||
if (time >= this.state.duration) {
|
||||
this.setState({ paused: true });
|
||||
this.setState({ paused: true }, this.handlePausedState);
|
||||
this.onEnd();
|
||||
} else {
|
||||
this.seekTo(time);
|
||||
|
@ -251,18 +274,29 @@ class MediaPlayer extends React.PureComponent {
|
|||
this.initSeeker();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { isPlayerVisible } = nextProps;
|
||||
if (!isPlayerVisible && !this.state.backgroundPlayEnabled) {
|
||||
// force pause if the player is not visible and background play is not enabled
|
||||
this.setState({ paused: true });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { assignPlayer } = this.props;
|
||||
const { assignPlayer, backgroundPlayEnabled } = this.props;
|
||||
if (assignPlayer) {
|
||||
assignPlayer(this);
|
||||
}
|
||||
|
||||
this.setState({ backgroundPlayEnabled: !!backgroundPlayEnabled });
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
AppState.addEventListener('change', this.handleAppStateChange);
|
||||
DeviceEventEmitter.addListener('onBackgroundPlayPressed', this.play);
|
||||
DeviceEventEmitter.addListener('onBackgroundPausePressed', this.pause);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.handleAppStateChange);
|
||||
DeviceEventEmitter.removeListener('onBackgroundPlayPressed', this.play);
|
||||
DeviceEventEmitter.removeListener('onBackgroundPausePressed', this.pause);
|
||||
this.clearControlsTimeout();
|
||||
|
@ -273,6 +307,26 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleAppStateChange = () => {
|
||||
if (AppState.currentState && AppState.currentState.match(/inactive|background/)) {
|
||||
if (!this.state.backgroundPlayEnabled && !this.state.paused) {
|
||||
this.setState({ paused: true, autoPaused: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (AppState.currentState && AppState.currentState.match(/active/)) {
|
||||
if (!this.state.backgroundPlayEnabled && this.state.autoPaused) {
|
||||
this.setState({ paused: false, autoPaused: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onBuffer = () => {
|
||||
if (!this.state.paused) {
|
||||
this.setState({ buffering: true }, () => this.manualHidePlayerControls());
|
||||
}
|
||||
}
|
||||
|
||||
play = () => {
|
||||
this.setState({ paused: false }, this.updateBackgroundMediaNotification);
|
||||
}
|
||||
|
@ -282,6 +336,7 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
|
||||
updateBackgroundMediaNotification = () => {
|
||||
this.handlePausedState();
|
||||
const { backgroundPlayEnabled } = this.props;
|
||||
if (backgroundPlayEnabled) {
|
||||
if (NativeModules.BackgroundMedia && window.currentMediaInfo) {
|
||||
|
@ -292,13 +347,19 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
|
||||
renderPlayerControls() {
|
||||
const { onBackButtonPressed } = this.props;
|
||||
|
||||
if (this.state.areControlsVisible) {
|
||||
return (
|
||||
<View style={mediaPlayerStyle.playerControlsContainer}>
|
||||
<TouchableOpacity style={mediaPlayerStyle.backButton} onPress={onBackButtonPressed}>
|
||||
<Icon name={"arrow-left"} size={18} style={mediaPlayerStyle.backButtonIcon} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.playPauseButton}
|
||||
onPress={this.togglePlay}>
|
||||
{this.state.paused && <Icon name="play" size={32} color="#ffffff" />}
|
||||
{!this.state.paused && <Icon name="pause" size={32} color="#ffffff" />}
|
||||
{this.state.paused && <Icon name="play" size={40} color="#ffffff" />}
|
||||
{!this.state.paused && <Icon name="pause" size={40} color="#ffffff" />}
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity style={mediaPlayerStyle.toggleFullscreenButton} onPress={this.toggleFullscreenMode}>
|
||||
|
@ -315,18 +376,6 @@ class MediaPlayer extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
getEncodedDownloadPath = (fileInfo) => {
|
||||
if (this.state.encodedFilePath) {
|
||||
return this.state.encodedFilePath;
|
||||
}
|
||||
|
||||
const { file_name: fileName } = fileInfo;
|
||||
const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21');
|
||||
const encodedFilePath = fileInfo.download_path.replace(fileName, encodedFileName);
|
||||
this.setState({ encodedFilePath });
|
||||
return encodedFilePath;
|
||||
}
|
||||
|
||||
onSeekerTouchAreaPressed = (evt) => {
|
||||
if (evt && evt.nativeEvent) {
|
||||
const newSeekerPosition = evt.nativeEvent.locationX;
|
||||
|
@ -338,8 +387,14 @@ class MediaPlayer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
onTrackingLayout = (evt) => {
|
||||
this.trackingOffset = evt.nativeEvent.layout.x;
|
||||
this.seekerWidth = evt.nativeEvent.layout.width;
|
||||
this.setSeekerPosition(this.calculateSeekerPosition());
|
||||
}
|
||||
|
||||
render() {
|
||||
const { backgroundPlayEnabled, fileInfo, thumbnail, onLayout, style } = this.props;
|
||||
const { onLayout, source, style, thumbnail } = this.props;
|
||||
const completedWidth = this.getCurrentTimePercentage() * this.seekerWidth;
|
||||
const remainingWidth = this.seekerWidth - completedWidth;
|
||||
let styles = [this.state.fullscreenMode ? mediaPlayerStyle.fullscreenContainer : mediaPlayerStyle.container];
|
||||
|
@ -356,17 +411,27 @@ class MediaPlayer extends React.PureComponent {
|
|||
|
||||
return (
|
||||
<View style={styles} onLayout={onLayout}>
|
||||
<Video source={{ uri: 'file:///' + this.getEncodedDownloadPath(fileInfo) }}
|
||||
<Video source={{
|
||||
uri: source,
|
||||
headers: {
|
||||
"Save-Data": "on",
|
||||
"Accept": "*/*"
|
||||
}
|
||||
}}
|
||||
bufferConfig={{ minBufferMs: 3000, maxBufferMs: 60000, bufferForPlaybackMs: 3000, bufferForPlaybackAfterRebufferMs: 3000 }}
|
||||
ref={(ref: Video) => { this.video = ref; }}
|
||||
resizeMode={this.state.resizeMode}
|
||||
playInBackground={backgroundPlayEnabled}
|
||||
playInBackground={this.state.backgroundPlayEnabled}
|
||||
style={mediaPlayerStyle.player}
|
||||
rate={this.state.rate}
|
||||
volume={this.state.volume}
|
||||
paused={this.state.paused}
|
||||
onLoad={this.onLoad}
|
||||
onBuffer={this.onBuffer}
|
||||
onProgress={this.onProgress}
|
||||
onEnd={this.onEnd}
|
||||
onError={this.onError}
|
||||
minLoadRetryCount={999}
|
||||
/>
|
||||
|
||||
{this.state.firstPlay && thumbnail && thumbnail.trim().length > 0 &&
|
||||
|
@ -381,16 +446,18 @@ class MediaPlayer extends React.PureComponent {
|
|||
</TouchableOpacity>
|
||||
|
||||
{(!this.state.fullscreenMode || (this.state.fullscreenMode && this.state.areControlsVisible)) &&
|
||||
<View style={trackingStyle} onLayout={(evt) => {
|
||||
this.trackingOffset = evt.nativeEvent.layout.x;
|
||||
this.seekerWidth = evt.nativeEvent.layout.width;
|
||||
}}>
|
||||
<View style={trackingStyle} onLayout={this.onTrackingLayout}>
|
||||
<View style={mediaPlayerStyle.progress}>
|
||||
<View style={[mediaPlayerStyle.innerProgressCompleted, { width: completedWidth }]} />
|
||||
<View style={[mediaPlayerStyle.innerProgressRemaining, { width: remainingWidth }]} />
|
||||
</View>
|
||||
</View>}
|
||||
|
||||
{this.state.buffering &&
|
||||
<View style={mediaPlayerStyle.loadingContainer}>
|
||||
<ActivityIndicator color={Colors.LbryGreen} size="large" />
|
||||
</View>}
|
||||
|
||||
{this.state.areControlsVisible &&
|
||||
<View style={{ left: this.getTrackingOffset(), width: this.seekerWidth }}>
|
||||
<View style={[mediaPlayerStyle.seekerHandle,
|
||||
|
|
|
@ -9,8 +9,8 @@ import {
|
|||
View
|
||||
} from 'react-native';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import NavigationButton from '../navigationButton';
|
||||
import pageHeaderStyle from '../../styles/pageHeader';
|
||||
import NavigationButton from 'component/navigationButton';
|
||||
import pageHeaderStyle from 'styles/pageHeader';
|
||||
|
||||
const APPBAR_HEIGHT = Platform.OS === 'ios' ? 44 : 56;
|
||||
const AnimatedText = Animated.Text;
|
||||
|
|
|
@ -21,7 +21,7 @@ class RewardEnrolment extends React.Component {
|
|||
|
||||
onEnrollPressed = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate({ routeName: 'Verification' })
|
||||
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: false }});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -66,6 +66,10 @@ class StorageStatsCard extends React.PureComponent {
|
|||
}
|
||||
|
||||
render() {
|
||||
if (this.state.totalBytes == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={storageStatsStyle.card}>
|
||||
<View style={[storageStatsStyle.row, storageStatsStyle.totalSizeContainer]}>
|
||||
|
|
|
@ -9,7 +9,7 @@ import discoverStyle from 'styles/discover';
|
|||
import uriBarStyle from 'styles/uriBar';
|
||||
|
||||
class UriBar extends React.PureComponent {
|
||||
static INPUT_TIMEOUT = 1000; // 1 second
|
||||
static INPUT_TIMEOUT = 2500; // 2.5 seconds
|
||||
|
||||
textInput = null;
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ class WalletAddress extends React.PureComponent<Props> {
|
|||
<Address address={receiveAddress} style={walletStyle.bottomMarginSmall} />
|
||||
<Button style={[walletStyle.button, walletStyle.bottomMarginLarge]}
|
||||
icon={'sync'}
|
||||
text={'Get New Address'}
|
||||
text={'Get new address'}
|
||||
onPress={getNewAddress}
|
||||
disabled={gettingNewAddress}
|
||||
/>
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { connect } from 'react-redux';
|
||||
import WalletRewardsDriver from './view';
|
||||
|
||||
const select = state => ({});
|
||||
|
||||
export default connect(select, null)(WalletRewardsDriver);
|
||||
export default connect()(WalletRewardsDriver);
|
||||
|
|
|
@ -108,6 +108,7 @@ class WalletSend extends React.PureComponent<Props> {
|
|||
<TextInput ref={ref => this.amountInput = ref}
|
||||
onChangeText={value => this.setState({amount: value})}
|
||||
keyboardType={'numeric'}
|
||||
placeholder={'0'}
|
||||
value={this.state.amount}
|
||||
style={[walletStyle.input, walletStyle.amountInput]} />
|
||||
<Text style={[walletStyle.text, walletStyle.currency]}>LBC</Text>
|
||||
|
|
10
app/src/component/walletSyncDriver/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import Constants from 'constants';
|
||||
import WalletSyncDriver from './view';
|
||||
|
||||
const select = state => ({
|
||||
deviceWalletSynced: makeSelectClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED)(state),
|
||||
});
|
||||
|
||||
export default connect(select, null)(WalletSyncDriver);
|
29
app/src/component/walletSyncDriver/view.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import Button from 'component/button';
|
||||
import Link from 'component/link';
|
||||
import walletStyle from 'styles/wallet';
|
||||
|
||||
class WalletSyncDriver extends React.PureComponent<Props> {
|
||||
onEnableSyncPressed = () => {
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate({ routeName: 'Verification', key: 'verification', params: { syncFlow: true } });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { deviceWalletSynced } = this.props;
|
||||
|
||||
return (
|
||||
<View style={walletStyle.syncDriverCard}>
|
||||
<Text style={walletStyle.syncDriverTitle}>Wallet sync is {deviceWalletSynced ? 'on' : 'off'}.</Text>
|
||||
{!deviceWalletSynced &&
|
||||
<View style={walletStyle.actionRow}>
|
||||
<Button style={walletStyle.enrollButton} theme={"light"} text={"Enable"} onPress={this.onEnableSyncPressed} />
|
||||
<Link text="Manual backup" href="https://lbry.com/faq/how-to-backup-wallet#android" style={walletStyle.syncDriverText} />
|
||||
</View>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default WalletSyncDriver;
|
|
@ -25,12 +25,14 @@ const Constants = {
|
|||
SETTING_RATING_REMINDER_DISABLED: "ratingReminderDisabled",
|
||||
SETTING_BACKUP_DISMISSED: "backupDismissed",
|
||||
SETTING_REWARDS_NOT_INTERESTED: "rewardsNotInterested",
|
||||
SETTING_DEVICE_WALLET_SYNCED: "deviceWalletSynced",
|
||||
|
||||
ACTION_DELETE_COMPLETED_BLOBS: "DELETE_COMPLETED_BLOBS",
|
||||
ACTION_FIRST_RUN_PAGE_CHANGED: "FIRST_RUN_PAGE_CHANGED",
|
||||
|
||||
ACTION_PUSH_DRAWER_STACK: "PUSH_DRAWER_STACK",
|
||||
ACTION_POP_DRAWER_STACK: "POP_DRAWER_STACK",
|
||||
ACTION_SET_PLAYER_VISIBLE: "SET_PLAYER_VISIBLE",
|
||||
|
||||
PAGE_REWARDS: "rewards",
|
||||
PAGE_SETTINGS: "settings",
|
||||
|
|
|
@ -3,7 +3,6 @@ import { setJSExceptionHandler } from 'react-native-exception-handler';
|
|||
import { Provider, connect } from 'react-redux';
|
||||
import {
|
||||
AppRegistry,
|
||||
AppState,
|
||||
Text,
|
||||
View,
|
||||
NativeModules
|
||||
|
@ -12,6 +11,7 @@ import {
|
|||
Lbry,
|
||||
claimsReducer,
|
||||
contentReducer,
|
||||
fileReducer,
|
||||
fileInfoReducer,
|
||||
notificationsReducer,
|
||||
searchReducer,
|
||||
|
@ -28,7 +28,6 @@ import {
|
|||
userReducer
|
||||
} from 'lbryinc';
|
||||
import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
|
||||
import { createLogger } from 'redux-logger';
|
||||
import { AppNavigator } from 'component/AppNavigator';
|
||||
import { persistStore, autoRehydrate } from 'redux-persist';
|
||||
import AppWithNavigationState, { reactNavigationMiddleware } from './component/AppNavigator';
|
||||
|
@ -44,8 +43,7 @@ import thunk from 'redux-thunk';
|
|||
|
||||
const globalExceptionHandler = (error, isFatal) => {
|
||||
if (error && NativeModules.Firebase) {
|
||||
console.log(error);
|
||||
NativeModules.Firebase.logException(isFatal, error.message ? error.message : "No message", error);
|
||||
NativeModules.Firebase.logException(isFatal, error.message ? error.message : "No message", JSON.stringify(error));
|
||||
}
|
||||
};
|
||||
setJSExceptionHandler(globalExceptionHandler, true);
|
||||
|
@ -93,6 +91,7 @@ const reducers = combineReducers({
|
|||
content: contentReducer,
|
||||
costInfo: costInfoReducer,
|
||||
drawer: drawerReducer,
|
||||
file: fileReducer,
|
||||
fileInfo: fileInfoReducer,
|
||||
homepage: homepageReducer,
|
||||
nav: navigatorReducer,
|
||||
|
@ -107,7 +106,6 @@ const reducers = combineReducers({
|
|||
});
|
||||
|
||||
const bulkThunk = createBulkThunkMiddleware();
|
||||
const logger = createLogger({ collapsed: true });
|
||||
const middleware = [thunk, bulkThunk, reactNavigationMiddleware];
|
||||
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import { doFetchAccessToken, selectAccessToken, selectUserEmail } from 'lbryinc';
|
||||
import { doPushDrawerStack, doPopDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { selectDrawerStack } from 'redux/selectors/drawer';
|
||||
import AboutPage from './view';
|
||||
import Constants from 'constants';
|
||||
|
@ -17,6 +17,7 @@ const perform = dispatch => ({
|
|||
notify: data => dispatch(doToast(data)),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_ABOUT)),
|
||||
popDrawerStack: () => dispatch(doPopDrawerStack()),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(AboutPage);
|
|
@ -14,7 +14,10 @@ class AboutPage extends React.PureComponent {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pushDrawerStack();
|
||||
const { pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
|
||||
if (NativeModules.VersionInfo) {
|
||||
NativeModules.VersionInfo.getAppVersion().then(version => {
|
||||
this.setState({appVersion: version});
|
||||
|
|
|
@ -178,12 +178,10 @@ class ChannelPage extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<View style={channelPageStyle.container}>
|
||||
<UriBar value={uri} navigation={navigation} />
|
||||
|
||||
|
||||
<View style={channelPageStyle.viewContainer}>
|
||||
<View style={channelPageStyle.cover}>
|
||||
<Image
|
||||
|
|
|
@ -174,6 +174,9 @@ class DiscoverPage extends React.PureComponent {
|
|||
(<SectionList
|
||||
style={discoverStyle.scrollContainer}
|
||||
contentContainerStyle={discoverStyle.scrollPadding}
|
||||
initialNumToRender={4}
|
||||
maxToRenderPerBatch={4}
|
||||
removeClippedSubviews={true}
|
||||
renderItem={ ({item, index, section}) => (
|
||||
<CategoryList
|
||||
key={item}
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
selectMyClaimsWithoutChannels,
|
||||
selectIsFetchingFileList,
|
||||
} from 'lbry-redux';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import Constants from 'constants';
|
||||
import DownloadsPage from './view';
|
||||
|
||||
|
@ -17,7 +17,8 @@ const select = (state) => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
fileList: () => dispatch(doFileList()),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_MY_LBRY))
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_MY_LBRY)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(DownloadsPage);
|
||||
|
|
|
@ -25,8 +25,9 @@ class DownloadsPage extends React.PureComponent {
|
|||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { fileList, pushDrawerStack } = this.props;
|
||||
const { fileList, pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
fileList();
|
||||
}
|
||||
|
||||
|
@ -39,7 +40,7 @@ class DownloadsPage extends React.PureComponent {
|
|||
<UriBar navigation={navigation} />
|
||||
{!fetching && !hasDownloads &&
|
||||
<View style={downloadsStyle.busyContainer}>
|
||||
<Text style={downloadsStyle.noDownloadsText}>You have not downloaded anything from LBRY yet.</Text>
|
||||
<Text style={downloadsStyle.noDownloadsText}>You have not watched or downloaded any content from LBRY yet.</Text>
|
||||
</View>}
|
||||
{fetching && !hasDownloads &&
|
||||
<View style={downloadsStyle.busyContainer}>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {
|
||||
doFetchFileInfo,
|
||||
doPurchaseUri,
|
||||
doDeletePurchasedUri,
|
||||
doResolveUri,
|
||||
doSendTip,
|
||||
doToast,
|
||||
|
@ -11,9 +13,13 @@ import {
|
|||
makeSelectContentPositionForUri,
|
||||
makeSelectContentTypeForUri,
|
||||
makeSelectMetadataForUri,
|
||||
makeSelectStreamingUrlForUri,
|
||||
makeSelectThumbnailForUri,
|
||||
makeSelectTitleForUri,
|
||||
selectBalance,
|
||||
selectPurchasedUris,
|
||||
selectFailedPurchaseUris,
|
||||
selectPurchaseUriErrorMessage,
|
||||
} from 'lbry-redux';
|
||||
import {
|
||||
doFetchCostInfoForUri,
|
||||
|
@ -21,7 +27,15 @@ import {
|
|||
selectRewardContentClaimIds,
|
||||
selectBlackListedOutpoints
|
||||
} from 'lbryinc';
|
||||
import { doDeleteFile, doPurchaseUri, doStopDownloadingFile } from 'redux/actions/file';
|
||||
import {
|
||||
doStartDownload,
|
||||
doUpdateDownload,
|
||||
doCompleteDownload,
|
||||
doDeleteFile,
|
||||
doStopDownloadingFile
|
||||
} from 'redux/actions/file';
|
||||
import { doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { selectDrawerStack } from 'redux/selectors/drawer';
|
||||
import FilePage from './view';
|
||||
|
||||
const select = (state, props) => {
|
||||
|
@ -30,6 +44,7 @@ const select = (state, props) => {
|
|||
balance: selectBalance(state),
|
||||
blackListedOutpoints: selectBlackListedOutpoints(state),
|
||||
claim: makeSelectClaimForUri(selectProps.uri)(state),
|
||||
drawerStack: selectDrawerStack(state),
|
||||
isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state),
|
||||
contentType: makeSelectContentTypeForUri(selectProps.uri)(state),
|
||||
costInfo: makeSelectCostInfoForUri(selectProps.uri)(state),
|
||||
|
@ -40,6 +55,10 @@ const select = (state, props) => {
|
|||
rewardedContentClaimIds: selectRewardContentClaimIds(state, selectProps),
|
||||
channelUri: makeSelectChannelForClaimUri(selectProps.uri, true)(state),
|
||||
position: makeSelectContentPositionForUri(selectProps.uri)(state),
|
||||
purchasedUris: selectPurchasedUris(state),
|
||||
failedPurchaseUris: selectFailedPurchaseUris(state),
|
||||
purchaseUriErrorMessage: selectPurchaseUriErrorMessage(state),
|
||||
streamingUrl: makeSelectStreamingUrlForUri(selectProps.uri)(state),
|
||||
thumbnail: makeSelectThumbnailForUri(selectProps.uri)(state),
|
||||
title: makeSelectTitleForUri(selectProps.uri)(state),
|
||||
};
|
||||
|
@ -52,10 +71,16 @@ const perform = dispatch => ({
|
|||
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
|
||||
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
purchaseUri: (uri, failureCallback) => dispatch(doPurchaseUri(uri, null, failureCallback)),
|
||||
popDrawerStack: () => dispatch(doPopDrawerStack()),
|
||||
purchaseUri: (uri, costInfo, saveFile) => dispatch(doPurchaseUri(uri, costInfo, saveFile)),
|
||||
deletePurchasedUri: uri => dispatch(doDeletePurchasedUri(uri)),
|
||||
resolveUri: uri => dispatch(doResolveUri(uri)),
|
||||
sendTip: (amount, claimId, uri, successCallback, errorCallback) => dispatch(doSendTip(amount, claimId, uri, successCallback, errorCallback)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(true)),
|
||||
stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)),
|
||||
startDownload: (uri, outpoint, fileInfo) => dispatch(doStartDownload(uri, outpoint, fileInfo)),
|
||||
updateDownload: (uri, outpoint, fileInfo, progress) => dispatch(doUpdateDownload(uri, outpoint, fileInfo, progress)),
|
||||
completeDownload: (uri, outpoint, fileInfo) => dispatch(doCompleteDownload(uri, outpoint, fileInfo)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(FilePage);
|
||||
|
|
|
@ -4,6 +4,7 @@ import { Lbryio } from 'lbryinc';
|
|||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
DeviceEventEmitter,
|
||||
Dimensions,
|
||||
NativeModules,
|
||||
ScrollView,
|
||||
|
@ -11,11 +12,13 @@ import {
|
|||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
WebView
|
||||
} from 'react-native';
|
||||
import { navigateToUri } from 'utils/helper';
|
||||
import { NavigationEvents } from 'react-navigation';
|
||||
import { navigateBack, navigateToUri } from 'utils/helper';
|
||||
import Icon from 'react-native-vector-icons/FontAwesome5';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import Button from 'component/button';
|
||||
|
@ -33,6 +36,7 @@ import SubscribeButton from 'component/subscribeButton';
|
|||
import SubscribeNotificationButton from 'component/subscribeNotificationButton';
|
||||
import UriBar from 'component/uriBar';
|
||||
import Video from 'react-native-video';
|
||||
import FileRewardsDriver from 'component/fileRewardsDriver';
|
||||
import filePageStyle from 'styles/filePage';
|
||||
import uriBarStyle from 'styles/uriBar';
|
||||
|
||||
|
@ -72,14 +76,19 @@ class FilePage extends React.PureComponent {
|
|||
tipAmount: null,
|
||||
uri: null,
|
||||
uriVars: null,
|
||||
stopDownloadConfirmed: false
|
||||
stopDownloadConfirmed: false,
|
||||
streamingMode: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
StatusBar.setHidden(false);
|
||||
|
||||
const { isResolvingUri, resolveUri, navigation } = this.props;
|
||||
DeviceEventEmitter.addListener('onDownloadStarted', this.handleDownloadStarted);
|
||||
DeviceEventEmitter.addListener('onDownloadUpdated', this.handleDownloadUpdated);
|
||||
DeviceEventEmitter.addListener('onDownloadCompleted', this.handleDownloadCompleted);
|
||||
|
||||
const { fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
|
||||
const { uri, uriVars } = navigation.state.params;
|
||||
this.setState({ uri, uriVars });
|
||||
|
||||
|
@ -96,8 +105,46 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const {
|
||||
claim,
|
||||
failedPurchaseUris: prevFailedPurchaseUris,
|
||||
purchasedUris: prevPurchasedUris,
|
||||
navigation,
|
||||
contentType,
|
||||
notify
|
||||
} = this.props;
|
||||
const { uri } = navigation.state.params;
|
||||
const { failedPurchaseUris, fileInfo, purchasedUris, purchaseUriErrorMessage, streamingUrl } = nextProps;
|
||||
|
||||
if (failedPurchaseUris.includes(uri) && !purchasedUris.includes(uri)) {
|
||||
if (purchaseUriErrorMessage && purchaseUriErrorMessage.trim().length > 0) {
|
||||
notify({ message: purchaseUriErrorMessage, isError: true });
|
||||
}
|
||||
this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false });
|
||||
}
|
||||
|
||||
const mediaType = Lbry.getMediaType(contentType);
|
||||
const isPlayable = mediaType === 'video' || mediaType === 'audio';
|
||||
if (prevPurchasedUris.length != purchasedUris.length && NativeModules.UtilityModule) {
|
||||
if (purchasedUris.includes(uri)) {
|
||||
const { nout, txid } = claim;
|
||||
const outpoint = `${txid}:${nout}`;
|
||||
NativeModules.UtilityModule.queueDownload(outpoint);
|
||||
}
|
||||
NativeModules.UtilityModule.checkDownloads();
|
||||
}
|
||||
|
||||
if (!this.state.streamingMode && isPlayable) {
|
||||
if (streamingUrl) {
|
||||
this.setState({ streamingMode: true, currentStreamUrl: streamingUrl });
|
||||
} else if (fileInfo && fileInfo.streaming_url) {
|
||||
this.setState({ streamingMode: true, currentStreamUrl: fileInfo.streaming_url });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
this.fetchFileInfo(this.props);
|
||||
const { claim, contentType, fileInfo, isResolvingUri, resolveUri, navigation } = this.props;
|
||||
const { uri } = this.state;
|
||||
if (!isResolvingUri && claim === undefined && uri) {
|
||||
|
@ -116,12 +163,11 @@ class FilePage extends React.PureComponent {
|
|||
|
||||
const prevFileInfo = prevProps.fileInfo;
|
||||
if (!prevFileInfo && fileInfo) {
|
||||
// started downloading
|
||||
const mediaType = Lbry.getMediaType(contentType);
|
||||
const isPlayable = mediaType === 'video' || mediaType === 'audio';
|
||||
// If the media is playable, file/view will be done in onPlaybackStarted
|
||||
if (!isPlayable && !this.state.fileViewLogged) {
|
||||
this.logFileView(uri, fileInfo);
|
||||
this.logFileView(uri, claim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +207,7 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
|
||||
onDeletePressed = () => {
|
||||
const { deleteFile, fileInfo } = this.props;
|
||||
const { claim, deleteFile, deletePurchasedUri, fileInfo, navigation } = this.props;
|
||||
|
||||
Alert.alert(
|
||||
'Delete file',
|
||||
|
@ -169,7 +215,12 @@ class FilePage extends React.PureComponent {
|
|||
[
|
||||
{ text: 'No' },
|
||||
{ text: 'Yes', onPress: () => {
|
||||
deleteFile(fileInfo.outpoint, true);
|
||||
const { uri } = navigation.state.params;
|
||||
deleteFile(`${claim.txid}:${claim.nout}`, true);
|
||||
deletePurchasedUri(uri);
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.deleteDownload(uri);
|
||||
}
|
||||
this.setState({
|
||||
downloadPressed: false,
|
||||
fileViewLogged: false,
|
||||
|
@ -183,7 +234,7 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
|
||||
onStopDownloadPressed = () => {
|
||||
const { fileInfo, navigation, notify, stopDownload } = this.props;
|
||||
const { deletePurchasedUri, fileInfo, navigation, notify, stopDownload } = this.props;
|
||||
|
||||
Alert.alert(
|
||||
'Stop download',
|
||||
|
@ -191,7 +242,12 @@ class FilePage extends React.PureComponent {
|
|||
[
|
||||
{ text: 'No' },
|
||||
{ text: 'Yes', onPress: () => {
|
||||
stopDownload(navigation.state.params.uri, fileInfo);
|
||||
const { uri } = navigation.state.params;
|
||||
stopDownload(uri, fileInfo);
|
||||
deletePurchasedUri(uri);
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.deleteDownload(uri);
|
||||
}
|
||||
this.setState({
|
||||
downloadPressed: false,
|
||||
fileViewLogged: false,
|
||||
|
@ -224,6 +280,28 @@ class FilePage extends React.PureComponent {
|
|||
window.currentMediaInfo = null;
|
||||
}
|
||||
window.player = null;
|
||||
|
||||
DeviceEventEmitter.removeListener('onDownloadStarted', this.handleDownloadStarted);
|
||||
DeviceEventEmitter.removeListener('onDownloadUpdated', this.handleDownloadUpdated);
|
||||
DeviceEventEmitter.removeListener('onDownloadCompleted', this.handleDownloadCompleted);
|
||||
}
|
||||
|
||||
handleDownloadStarted = (evt) => {
|
||||
const { startDownload } = this.props;
|
||||
const { uri, outpoint, fileInfo } = evt;
|
||||
startDownload(uri, outpoint, fileInfo);
|
||||
}
|
||||
|
||||
handleDownloadUpdated = (evt) => {
|
||||
const { updateDownload } = this.props;
|
||||
const { uri, outpoint, fileInfo, progress } = evt;
|
||||
updateDownload(uri, outpoint, fileInfo, progress);
|
||||
}
|
||||
|
||||
handleDownloadCompleted = (evt) => {
|
||||
const { completeDownload } = this.props;
|
||||
const { uri, outpoint, fileInfo } = evt;
|
||||
completeDownload(uri, outpoint, fileInfo);
|
||||
}
|
||||
|
||||
localUriForFileInfo = (fileInfo) => {
|
||||
|
@ -233,6 +311,33 @@ class FilePage extends React.PureComponent {
|
|||
return 'file:///' + fileInfo.download_path;
|
||||
}
|
||||
|
||||
playerUriForFileInfo = (fileInfo) => {
|
||||
const { streamingUrl } = this.props;
|
||||
if (streamingUrl) {
|
||||
return streamingUrl;
|
||||
}
|
||||
if (this.state.currentStreamUrl) {
|
||||
return this.state.currentStreamUrl;
|
||||
}
|
||||
|
||||
if (fileInfo && fileInfo.download_path) {
|
||||
return this.getEncodedDownloadPath(fileInfo);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
getEncodedDownloadPath = (fileInfo) => {
|
||||
if (this.state.encodedFilePath) {
|
||||
return this.state.encodedFilePath;
|
||||
}
|
||||
|
||||
const { file_name: fileName } = fileInfo;
|
||||
const encodedFileName = encodeURIComponent(fileName).replace(/!/g, '%21');
|
||||
const encodedFilePath = fileInfo.download_path.replace(fileName, encodedFileName);
|
||||
return encodedFilePath;
|
||||
}
|
||||
|
||||
linkify = (text) => {
|
||||
let linkifiedContent = [];
|
||||
let lines = text.split(/\n/g);
|
||||
|
@ -321,8 +426,13 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
logFileView = (uri, fileInfo, timeToStart) => {
|
||||
const { outpoint, claim_id: claimId } = fileInfo;
|
||||
logFileView = (uri, claim, timeToStart) => {
|
||||
if (!claim) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { nout, claim_id: claimId, txid } = claim;
|
||||
const outpoint = `${txid}:${nout}`;
|
||||
const params = {
|
||||
uri,
|
||||
outpoint,
|
||||
|
@ -351,23 +461,30 @@ class FilePage extends React.PureComponent {
|
|||
sendTip(tipAmount, claim.claim_id, uri, () => { this.setState({ tipAmount: 0, showTipView: false }) });
|
||||
}
|
||||
|
||||
startDownloadFailed = () => {
|
||||
this.startTime = null;
|
||||
setTimeout(() => {
|
||||
this.setState({ downloadPressed: false, fileViewLogged: false, mediaLoaded: false });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
renderTags = (tags) => {
|
||||
return tags.map((tag, i) => (
|
||||
<Text style={filePageStyle.tagItem} key={`${tag}-${i}`}>{tag}</Text>
|
||||
));
|
||||
}
|
||||
|
||||
onFileDownloadButtonPlayed = () => {
|
||||
const { setPlayerVisible } = this.props;
|
||||
this.startTime = Date.now();
|
||||
this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false });
|
||||
setPlayerVisible();
|
||||
}
|
||||
|
||||
onBackButtonPressed = () => {
|
||||
const { navigation, drawerStack, popDrawerStack } = this.props;
|
||||
navigateBack(navigation, drawerStack, popDrawerStack);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
balance,
|
||||
claim,
|
||||
channelUri,
|
||||
costInfo,
|
||||
fileInfo,
|
||||
metadata,
|
||||
contentType,
|
||||
|
@ -442,7 +559,7 @@ class FilePage extends React.PureComponent {
|
|||
const mediaType = Lbry.getMediaType(contentType);
|
||||
const isPlayable = mediaType === 'video' || mediaType === 'audio';
|
||||
const { height, channel_name: channelName, value } = claim;
|
||||
const showActions = !this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView;
|
||||
const showActions = !this.state.streamingMode && !this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView;
|
||||
const showFileActions = (completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
|
||||
const channelClaimId = claim && claim.signing_channel && claim.signing_channel.claim_id;
|
||||
const canSendTip = this.state.tipAmount > 0;
|
||||
|
@ -454,8 +571,8 @@ class FilePage extends React.PureComponent {
|
|||
const playerBgStyle = [filePageStyle.playerBackground, filePageStyle.containedPlayerBackground];
|
||||
const fsPlayerBgStyle = [filePageStyle.playerBackground, filePageStyle.fullscreenPlayerBackground];
|
||||
// at least 2MB (or the full download) before media can be loaded
|
||||
const canLoadMedia = fileInfo &&
|
||||
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes); // 2MB = 1024*1024*2
|
||||
const canLoadMedia = (this.state.streamingMode) || (fileInfo &&
|
||||
(fileInfo.written_bytes >= 2097152 || fileInfo.written_bytes == fileInfo.total_bytes)); // 2MB = 1024*1024*2
|
||||
const isViewable = (mediaType === 'image' || mediaType === 'text');
|
||||
const isWebViewable = mediaType === 'text';
|
||||
const canOpen = isViewable && completed;
|
||||
|
@ -485,7 +602,10 @@ class FilePage extends React.PureComponent {
|
|||
|
||||
if (fileInfo && !this.state.autoDownloadStarted && this.state.uriVars && 'true' === this.state.uriVars.download) {
|
||||
this.setState({ autoDownloadStarted: true }, () => {
|
||||
purchaseUri(uri, this.startDownloadFailed);
|
||||
purchaseUri(uri, costInfo, !isPlayable);
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.checkDownloads();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -512,22 +632,23 @@ class FilePage extends React.PureComponent {
|
|||
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={thumbnail} />}
|
||||
{((!this.state.downloadButtonShown || this.state.downloadPressed) && !this.state.mediaLoaded) &&
|
||||
<ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
|
||||
{((isPlayable && !completed && !canLoadMedia) || !completed || canOpen) && (!this.state.downloadPressed) &&
|
||||
{((isPlayable && !completed && !canLoadMedia) || canOpen || (!completed && !this.state.streamingMode)) &&
|
||||
(!this.state.downloadPressed) &&
|
||||
<FileDownloadButton uri={uri}
|
||||
style={filePageStyle.downloadButton}
|
||||
openFile={openFile}
|
||||
isPlayable={isPlayable}
|
||||
isViewable={isViewable}
|
||||
onPlay={() => {
|
||||
this.startTime = Date.now();
|
||||
this.setState({ downloadPressed: true, autoPlayMedia: true, stopDownloadConfirmed: false });
|
||||
}}
|
||||
onPlay={this.onFileDownloadButtonPlayed}
|
||||
onView={() => this.setState({ downloadPressed: true })}
|
||||
onButtonLayout={() => this.setState({ downloadButtonShown: true })}
|
||||
onStartDownloadFailed={this.startDownloadFailed} />}
|
||||
onButtonLayout={() => this.setState({ downloadButtonShown: true })} />}
|
||||
{!fileInfo && <FilePrice uri={uri} style={filePageStyle.filePriceContainer} textStyle={filePageStyle.filePriceText} />}
|
||||
|
||||
<TouchableOpacity style={filePageStyle.backButton} onPress={this.onBackButtonPressed}>
|
||||
<Icon name={"arrow-left"} size={18} style={filePageStyle.backButtonIcon} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{(canLoadMedia && fileInfo && isPlayable) &&
|
||||
{(this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) &&
|
||||
<View style={playerBgStyle}
|
||||
ref={(ref) => { this.playerBackground = ref; }}
|
||||
onLayout={(evt) => {
|
||||
|
@ -535,12 +656,14 @@ class FilePage extends React.PureComponent {
|
|||
this.setState({ playerBgHeight: evt.nativeEvent.layout.height });
|
||||
}
|
||||
}} />}
|
||||
{(canLoadMedia && fileInfo && isPlayable && this.state.fullscreenMode) && <View style={fsPlayerBgStyle} />}
|
||||
{(canLoadMedia && fileInfo && isPlayable) &&
|
||||
{((this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) && this.state.fullscreenMode) &&
|
||||
<View style={fsPlayerBgStyle} />}
|
||||
{(this.state.streamingMode || (canLoadMedia && fileInfo && isPlayable)) &&
|
||||
<MediaPlayer
|
||||
fileInfo={fileInfo}
|
||||
claim={claim}
|
||||
assignPlayer={(ref) => { this.player = ref; }}
|
||||
uri={uri}
|
||||
source={this.playerUriForFileInfo(fileInfo)}
|
||||
style={playerStyle}
|
||||
autoPlay={autoplay || this.state.autoPlayMedia}
|
||||
onFullscreenToggled={this.handleFullscreenToggle}
|
||||
|
@ -550,6 +673,7 @@ class FilePage extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
onMediaLoaded={() => this.onMediaLoaded(channelName, title, uri)}
|
||||
onBackButtonPressed={this.onBackButtonPressed}
|
||||
onPlaybackStarted={this.onPlaybackStarted}
|
||||
onPlaybackFinished={this.onPlaybackFinished}
|
||||
thumbnail={thumbnail}
|
||||
|
@ -580,13 +704,15 @@ class FilePage extends React.PureComponent {
|
|||
style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}
|
||||
contentContainerstyle={showActions ? null : filePageStyle.scrollContent}
|
||||
ref={(ref) => { this.scrollView = ref; }}>
|
||||
<View style={filePageStyle.titleRow}>
|
||||
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
|
||||
<TouchableWithoutFeedback style={filePageStyle.descriptionToggle}
|
||||
onPress={() => this.setState({ showDescription: !this.state.showDescription })}>
|
||||
<Icon name={this.state.showDescription ? "caret-up" : "caret-down"} size={24} />
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
<TouchableWithoutFeedback style={filePageStyle.titleTouch}
|
||||
onPress={() => this.setState({ showDescription: !this.state.showDescription })}>
|
||||
<View style={filePageStyle.titleRow}>
|
||||
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
|
||||
<View style={filePageStyle.descriptionToggle}>
|
||||
<Icon name={this.state.showDescription ? "caret-up" : "caret-down"} size={24} />
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
{channelName &&
|
||||
<View style={filePageStyle.channelRow}>
|
||||
<View style={filePageStyle.publishInfo}>
|
||||
|
@ -630,6 +756,7 @@ class FilePage extends React.PureComponent {
|
|||
<TextInput ref={ref => this.tipAmountInput = ref}
|
||||
onChangeText={value => this.setState({tipAmount: value})}
|
||||
keyboardType={'numeric'}
|
||||
placeholder={'0'}
|
||||
value={this.state.tipAmount}
|
||||
style={[filePageStyle.input, filePageStyle.tipAmountInput]} />
|
||||
<Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text>
|
||||
|
@ -654,12 +781,15 @@ class FilePage extends React.PureComponent {
|
|||
)}
|
||||
</View>)}
|
||||
|
||||
{(costInfo && parseFloat(costInfo.cost) > balance) && <FileRewardsDriver navigation={navigation} />}
|
||||
|
||||
<View onLayout={this.setRelatedContentPosition} />
|
||||
<RelatedContent navigation={navigation} uri={uri} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)}
|
||||
{!this.state.fullscreenMode && <FloatingWalletBalance navigation={navigation} />}
|
||||
{(!this.state.fullscreenMode && !this.state.showImageViewer && !this.state.showWebView) &&
|
||||
<FloatingWalletBalance navigation={navigation} />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ import { doToast } from 'lbry-redux';
|
|||
import {
|
||||
doAuthenticate,
|
||||
doCheckSync,
|
||||
doGetSync,
|
||||
doSyncApply,
|
||||
doUserEmailNew,
|
||||
doUserResendVerificationEmail,
|
||||
selectAuthToken,
|
||||
|
@ -11,9 +13,14 @@ import {
|
|||
selectEmailToVerify,
|
||||
selectAuthenticationIsPending,
|
||||
selectHasSyncedWallet,
|
||||
selectIsRetrievingSync,
|
||||
selectGetSyncIsPending,
|
||||
selectSyncApplyIsPending,
|
||||
selectSyncApplyErrorMessage,
|
||||
selectSyncData,
|
||||
selectSyncHash,
|
||||
selectUser,
|
||||
} from 'lbryinc';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import FirstRun from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
|
@ -23,13 +30,20 @@ const select = (state) => ({
|
|||
emailNewErrorMessage: selectEmailNewErrorMessage(state),
|
||||
emailNewPending: selectEmailNewIsPending(state),
|
||||
hasSyncedWallet: selectHasSyncedWallet(state),
|
||||
isRetrievingSync: selectIsRetrievingSync(state),
|
||||
getSyncIsPending: selectGetSyncIsPending(state),
|
||||
syncApplyErrorMessage: selectSyncApplyErrorMessage(state),
|
||||
syncApplyIsPending: selectSyncApplyIsPending(state),
|
||||
syncHash: selectSyncHash(state),
|
||||
syncData: selectSyncData(state),
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
addUserEmail: email => dispatch(doUserEmailNew(email)),
|
||||
authenticate: (appVersion, os) => dispatch(doAuthenticate(appVersion, os)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
syncApply: (hash, data, password) => dispatch(doSyncApply(hash, data, password)),
|
||||
getSync: password => dispatch(doGetSync(password)),
|
||||
checkSync: () => dispatch(doCheckSync()),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email))
|
||||
|
|
|
@ -96,7 +96,7 @@ class EmailCollectPage extends React.PureComponent {
|
|||
);
|
||||
} else if (!authToken || authenticating || this.state.verifying) {
|
||||
content = (
|
||||
<View>
|
||||
<View style={firstRunStyle.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
|
||||
<Text style={firstRunStyle.paragraph}>Please wait while we get some things ready...</Text>
|
||||
</View>
|
||||
|
|
|
@ -16,6 +16,8 @@ import Colors from 'styles/colors';
|
|||
import Constants from 'constants';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
|
||||
const firstRunMargins = 80;
|
||||
LINTER LINTER
Sometimes, it's okay to forget. Sometimes, it's okay to forget.
|
||||
|
||||
class WalletPage extends React.PureComponent {
|
||||
state = {
|
||||
password: null,
|
||||
|
@ -27,20 +29,20 @@ class WalletPage extends React.PureComponent {
|
|||
|
||||
componentDidMount() {
|
||||
this.checkWalletReady();
|
||||
this.props.checkSync();
|
||||
setTimeout(() => this.setState({ hasCheckedSync: true}), 1000);
|
||||
}
|
||||
|
||||
checkWalletReady = () => {
|
||||
// make sure the sdk wallet component is ready
|
||||
Lbry.status().then(status => {
|
||||
if (status.startup_status && status.startup_status.wallet) {
|
||||
this.setState({ walletReady: true });
|
||||
this.setState({ walletReady: true }, () => {
|
||||
this.props.checkSync();
|
||||
setTimeout(() => this.setState({ hasCheckedSync: true}), 1000);
|
||||
});
|
||||
return;
|
||||
}
|
||||
setTimeout(this.checkWalletReady, 1000);
|
||||
}).catch((e) => {
|
||||
console.log(e);
|
||||
setTimeout(this.checkWalletReady, 1000);
|
||||
});
|
||||
}
|
||||
|
@ -52,23 +54,24 @@ class WalletPage extends React.PureComponent {
|
|||
if (onPasswordChanged) {
|
||||
onPasswordChanged(text);
|
||||
}
|
||||
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, text);
|
||||
// simply set any string value to indicate that a passphrase was set on first run
|
||||
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_PASSWORD, "true");
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { onPasswordChanged, onWalletViewLayout, isRetrievingSync, hasSyncedWallet } = this.props;
|
||||
const { onPasswordChanged, onWalletViewLayout, getSyncIsPending, hasSyncedWallet, syncApplyIsPending } = this.props;
|
||||
That 80 seems potentially evil That 80 seems potentially evil
Should definitely be a constant. It's the left and right first run margins (32 + 32), and the control padding (8 + 8), added together. Should definitely be a constant. It's the left and right first run margins (32 + 32), and the control padding (8 + 8), added together.
|
||||
|
||||
let content;
|
||||
if (!this.state.walletReady || !this.state.hasCheckedSync || isRetrievingSync) {
|
||||
if (!this.state.walletReady || !this.state.hasCheckedSync || getSyncIsPending) {
|
||||
content = (
|
||||
<View>
|
||||
<View style={firstRunStyle.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
|
||||
<Text style={firstRunStyle.paragraph}>Retrieving your account information...</Text>
|
||||
<Text style={firstRunStyle.paragraph}>Retrieving your account information...</Text>
|
||||
</View>
|
||||
);
|
||||
} else if (syncApplyIsPending) {
|
||||
content = (
|
||||
<View style={firstRunStyle.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
|
||||
<Text style={firstRunStyle.paragraph}>Validating password...</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
|
@ -96,10 +99,11 @@ class WalletPage extends React.PureComponent {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
{(this.state.password && this.state.password.trim().length) > 0 &&
|
||||
|
||||
{(!hasSyncedWallet && this.state.password && this.state.password.trim().length) > 0 &&
|
||||
<View style={firstRunStyle.passwordStrength}>
|
||||
<BarPasswordStrengthDisplay
|
||||
width={Dimensions.get('window').width - 80}
|
||||
width={Dimensions.get('window').width - firstRunMargins}
|
||||
minLength={1}
|
||||
password={this.state.password} />
|
||||
</View>}
|
||||
|
|
|
@ -38,7 +38,8 @@ class FirstRunScreen extends React.PureComponent {
|
|||
isEmailVerified: false,
|
||||
skipAccountConfirmed: false,
|
||||
showBottomContainer: true,
|
||||
walletPassword: null
|
||||
walletPassword: null,
|
||||
syncApplyStarted: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -65,12 +66,12 @@ class FirstRunScreen extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { emailNewErrorMessage, emailNewPending, user } = nextProps;
|
||||
const { notify } = this.props;
|
||||
const { emailNewErrorMessage, emailNewPending, syncApplyErrorMessage, syncApplyIsPending, user } = nextProps;
|
||||
const { notify, isApplyingSync, setClientSetting } = this.props;
|
||||
|
||||
if (this.state.emailSubmitted && !emailNewPending) {
|
||||
this.setState({ emailSubmitted: false });
|
||||
if (emailNewErrorMessage) {
|
||||
if (emailNewErrorMessage && emailNewErrorMessage.trim().length > 0) {
|
||||
notify ({ message: String(emailNewErrorMessage), isError: true });
|
||||
} else {
|
||||
// Request successful. Navigate to email verify page.
|
||||
|
@ -78,6 +79,21 @@ class FirstRunScreen extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
if (this.state.syncApplyStarted && !syncApplyIsPending) {
|
||||
this.setState({ syncApplyStarted: false });
|
||||
if (syncApplyErrorMessage && syncApplyErrorMessage.trim().length > 0) {
|
||||
notify({ message: syncApplyErrorMessage, isError: true });
|
||||
this.setState({ showBottomContainer: true });
|
||||
} else {
|
||||
// password successfully verified
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, this.state.walletPassword);
|
||||
}
|
||||
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
|
||||
this.closeFinalPage();
|
||||
}
|
||||
}
|
||||
|
||||
this.checkVerificationStatus(user);
|
||||
}
|
||||
|
||||
|
@ -121,15 +137,27 @@ class FirstRunScreen extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
checkWalletPassword = () => {
|
||||
const { syncApply, syncHash, syncData } = this.props;
|
||||
this.setState({ syncApplyStarted: true, showBottomContainer: false }, () => {
|
||||
syncApply(syncHash, syncData, this.state.walletPassword);
|
||||
});
|
||||
}
|
||||
|
||||
handleContinuePressed = () => {
|
||||
const { notify, user } = this.props;
|
||||
const { notify, user, hasSyncedWallet } = this.props;
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
|
||||
if (Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage) {
|
||||
if (!this.state.walletPassword || this.state.walletPassword.trim().length === 0) {
|
||||
return notify({ message: 'Please enter a wallet password' });
|
||||
}
|
||||
|
||||
this.closeFinalPage();
|
||||
// do apply sync to check if the password is valid
|
||||
if (hasSyncedWallet) {
|
||||
this.checkWalletPassword();
|
||||
} else {
|
||||
this.setFreshPassword();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -167,20 +195,25 @@ class FirstRunScreen extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
checkBottomContainer = (pageName) => {
|
||||
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === pageName || Constants.FIRST_RUN_PAGE_WALLET === pageName) {
|
||||
// do not show the buttons (because we're waiting to get things ready)
|
||||
this.setState({ showBottomContainer: false });
|
||||
}
|
||||
}
|
||||
|
||||
showNextPage = () => {
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
|
||||
const nextPage = FirstRunScreen.pages[pageIndex + 1];
|
||||
this.setState({ currentPage: nextPage });
|
||||
if (nextPage === Constants.FIRST_RUN_PAGE_EMAIL_COLLECT) {
|
||||
// do not show the buttons (because we're waiting to get things ready)
|
||||
this.setState({ showBottomContainer: false });
|
||||
}
|
||||
this.checkBottomContainer(nextPage);
|
||||
}
|
||||
|
||||
showPage(pageName) {
|
||||
const pageIndex = FirstRunScreen.pages.indexOf(pageName);
|
||||
if (pageIndex > -1) {
|
||||
this.setState({ currentPage: pageName });
|
||||
this.checkBottomContainer(pageName);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -222,6 +255,21 @@ class FirstRunScreen extends React.PureComponent {
|
|||
this.setState({ skipAccountConfirmed: checked });
|
||||
}
|
||||
|
||||
setFreshPassword = () => {
|
||||
const { getSync, setClientSetting } = this.props;
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, this.state.walletPassword);
|
||||
Lbry.account_encrypt({ new_password: this.state.walletPassword }).then(() => {
|
||||
Lbry.account_unlock({ password: this.state.walletPassword }).then(() => {
|
||||
// fresh account, new password set
|
||||
getSync(this.state.walletPassword);
|
||||
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
|
||||
this.closeFinalPage();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
authenticate,
|
||||
|
@ -233,7 +281,8 @@ class FirstRunScreen extends React.PureComponent {
|
|||
emailToVerify,
|
||||
notify,
|
||||
hasSyncedWallet,
|
||||
isRetrievingSync,
|
||||
getSyncIsPending,
|
||||
syncApplyIsPending,
|
||||
resendVerificationEmail,
|
||||
user
|
||||
} = this.props;
|
||||
Inconsistent casing. I think we generally do not capitalize the second word on actions like this, but what's important is just being consistent. Inconsistent casing. I think we generally do not capitalize the second word on actions like this, but what's important is just being consistent.
|
||||
|
@ -267,7 +316,8 @@ class FirstRunScreen extends React.PureComponent {
|
|||
page = (<WalletPage
|
||||
checkSync={checkSync}
|
||||
hasSyncedWallet={hasSyncedWallet}
|
||||
isRetrievingSync={isRetrievingSync}
|
||||
getSyncIsPending={getSyncIsPending}
|
||||
syncApplyIsPending={syncApplyIsPending}
|
||||
onWalletViewLayout={this.onWalletViewLayout}
|
||||
onPasswordChanged={this.onWalletPasswordChanged} />);
|
||||
break;
|
||||
|
@ -293,7 +343,7 @@ class FirstRunScreen extends React.PureComponent {
|
|||
Constants.FIRST_RUN_PAGE_EMAIL_VERIFY === this.state.currentPage) &&
|
||||
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
|
||||
<Text style={firstRunStyle.buttonText}>
|
||||
« {Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage ? 'Setup account' : 'Change Email'}</Text>
|
||||
« {Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage ? 'Setup account' : 'Change email'}</Text>
|
||||
</TouchableOpacity>}
|
||||
{!emailNewPending && (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) &&
|
||||
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
selectUser,
|
||||
} from 'lbryinc';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import Constants from 'constants';
|
||||
import RewardsPage from './view';
|
||||
|
||||
|
@ -27,7 +27,8 @@ const perform = dispatch => ({
|
|||
claimReward: reward => dispatch(doClaimRewardType(reward.reward_type, true)),
|
||||
fetchRewards: () => dispatch(doRewardList()),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_REWARDS))
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_REWARDS)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(RewardsPage);
|
||||
|
|
|
@ -32,9 +32,10 @@ class RewardsPage extends React.PureComponent {
|
|||
scrollView = null;
|
||||
|
||||
componentDidMount() {
|
||||
const { fetchRewards, pushDrawerStack, navigation, user } = this.props;
|
||||
const { fetchRewards, pushDrawerStack, navigation, setPlayerVisible, user } = this.props;
|
||||
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
fetchRewards();
|
||||
|
||||
this.setState({
|
||||
|
@ -156,10 +157,10 @@ class RewardsPage extends React.PureComponent {
|
|||
return (
|
||||
<View style={rewardStyle.container}>
|
||||
<UriBar navigation={navigation} />
|
||||
{(!this.state.isEmailVerified || !this.state.isIdentityVerified || !this.state.isRewardApproved) &&
|
||||
{(!this.state.isEmailVerified || !this.state.isRewardApproved) &&
|
||||
<RewardEnrolment navigation={navigation} />}
|
||||
|
||||
{(this.state.isEmailVerified && this.state.isIdentityVerified && this.state.isRewardApproved) &&
|
||||
{(this.state.isEmailVerified && this.state.isRewardApproved) &&
|
||||
<ScrollView
|
||||
ref={ref => this.scrollView = ref}
|
||||
keyboardShouldPersistTaps={'handled'}
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
makeSelectQueryWithOptions,
|
||||
selectSearchUrisByQuery
|
||||
} from 'lbry-redux';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import Constants from 'constants';
|
||||
import SearchPage from './view';
|
||||
|
||||
|
@ -23,6 +23,7 @@ const perform = dispatch => ({
|
|||
search: (query) => dispatch(doSearch(query, 25)),
|
||||
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query)),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SEARCH)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SearchPage);
|
||||
|
|
|
@ -26,12 +26,17 @@ class SearchPage extends React.PureComponent {
|
|||
};
|
||||
|
||||
componentWillMount() {
|
||||
this.props.pushDrawerStack();
|
||||
const { pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { navigation, search } = this.props;
|
||||
const { searchQuery } = navigation.state.params;
|
||||
let searchQuery;
|
||||
if (navigation && navigation.state) {
|
||||
searchQuery = navigation.state.params.searchQuery;
|
||||
}
|
||||
if (searchQuery && searchQuery.trim().length > 0) {
|
||||
this.setState({ currentUri: (isURIValid(searchQuery)) ? normalizeURI(searchQuery) : null })
|
||||
search(searchQuery);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { SETTINGS } from 'lbry-redux';
|
||||
import { doPushDrawerStack, doPopDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doPopDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectDrawerStack } from 'redux/selectors/drawer';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
|
@ -18,6 +18,7 @@ const perform = dispatch => ({
|
|||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SETTINGS)),
|
||||
popDrawerStack: () => dispatch(doPopDrawerStack()),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingsPage);
|
||||
|
|
|
@ -11,7 +11,9 @@ class SettingsPage extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.pushDrawerStack();
|
||||
const { pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
selectUser,
|
||||
selectEmailToVerify
|
||||
} from 'lbryinc';
|
||||
import { doDeleteCompleteBlobs } from 'redux/actions/file';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import SplashScreen from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
@ -26,11 +26,11 @@ const perform = dispatch => ({
|
|||
balanceSubscribe: () => dispatch(doBalanceSubscribe()),
|
||||
blacklistedOutpointsSubscribe: () => dispatch(doBlackListedOutpointsSubscribe()),
|
||||
checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()),
|
||||
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
|
||||
fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
|
||||
fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)),
|
||||
getSync: password => dispatch(doGetSync(password)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
|
||||
updateBlockHeight: () => dispatch(doUpdateBlockHeight()),
|
||||
verifyUserEmail: (token, recaptcha) => dispatch(doUserEmailVerify(token, recaptcha)),
|
||||
|
|
|
@ -28,7 +28,7 @@ class SplashScreen extends React.PureComponent {
|
|||
componentWillMount() {
|
||||
this.setState({
|
||||
daemonReady: false,
|
||||
details: 'Starting daemon',
|
||||
details: 'Starting up...',
|
||||
message: 'Connecting',
|
||||
Good removal of jargon Good removal of jargon
|
||||
isRunning: false,
|
||||
isLagging: false,
|
||||
|
@ -114,22 +114,18 @@ class SplashScreen extends React.PureComponent {
|
|||
|
||||
if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) {
|
||||
this.setState({ shouldAuthenticate: false }, () => {
|
||||
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
|
||||
if (email) {
|
||||
setEmailToVerify(email);
|
||||
}
|
||||
|
||||
// user is authenticated, navigate to the main view
|
||||
if (user.has_verified_email) {
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
// user is authenticated, navigate to the main view
|
||||
if (user.has_verified_email) {
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
if (walletPassword && walletPassword.trim().length > 0) {
|
||||
getSync(walletPassword);
|
||||
this.navigateToMain();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.navigateToMain();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.navigateToMain();
|
||||
});
|
||||
this.navigateToMain();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -140,9 +136,11 @@ class SplashScreen extends React.PureComponent {
|
|||
balanceSubscribe,
|
||||
blacklistedOutpointsSubscribe,
|
||||
checkSubscriptionsInit,
|
||||
updateBlockHeight,
|
||||
getSync,
|
||||
navigation,
|
||||
notify
|
||||
notify,
|
||||
updateBlockHeight,
|
||||
user
|
||||
} = this.props;
|
||||
|
||||
Lbry.resolve({ urls: 'lbry://one' }).then(() => {
|
||||
|
@ -152,21 +150,30 @@ class SplashScreen extends React.PureComponent {
|
|||
checkSubscriptionsInit();
|
||||
updateBlockHeight();
|
||||
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
|
||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||
this.setState({ shouldAuthenticate: true });
|
||||
authenticate(appVersion, Platform.OS);
|
||||
});
|
||||
|
||||
if (user && user.id && user.has_verified_email) {
|
||||
// user already authenticated
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
if (walletPassword && walletPassword.trim().length > 0) {
|
||||
getSync(walletPassword);
|
||||
}
|
||||
this.navigateToMain();
|
||||
});
|
||||
} else {
|
||||
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
|
||||
this.setState({ shouldAuthenticate: true });
|
||||
authenticate(appVersion, Platform.OS);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_updateStatusCallback(status) {
|
||||
const { deleteCompleteBlobs, fetchSubscriptions } = this.props;
|
||||
const { fetchSubscriptions, getSync, setClientSetting } = this.props;
|
||||
const startupStatus = status.startup_status;
|
||||
// At the minimum, wallet should be started and blocks_behind equal to 0 before calling resolve
|
||||
const hasStarted = startupStatus.stream_manager && startupStatus.wallet && status.wallet.blocks_behind <= 0;
|
||||
if (hasStarted) {
|
||||
deleteCompleteBlobs();
|
||||
|
||||
// Wait until we are able to resolve a name before declaring
|
||||
// that we are done.
|
||||
// TODO: This is a hack, and the logic should live in the daemon
|
||||
|
@ -179,37 +186,18 @@ class SplashScreen extends React.PureComponent {
|
|||
isRunning: true,
|
||||
});
|
||||
|
||||
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_PASSWORD).then(passwordSet => {
|
||||
if ("true" === passwordSet) {
|
||||
// encrypt the wallet
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
|
||||
if (!password || password.trim().length === 0) {
|
||||
this.finishSplashScreen();
|
||||
return;
|
||||
}
|
||||
|
||||
Lbry.account_encrypt({ new_password: password }).then((result) => {
|
||||
AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_PASSWORD);
|
||||
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
|
||||
});
|
||||
});
|
||||
|
||||
// For now, automatically unlock the wallet if a password is set so that downloads work
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
|
||||
if (password && password.trim().length > 0) {
|
||||
// unlock the wallet and then finish the splash screen
|
||||
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen()).catch(() => this.finishSplashScreen());
|
||||
return;
|
||||
}
|
||||
|
||||
// For now, automatically unlock the wallet if a password is set so that downloads work
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
|
||||
if (password && password.trim().length > 0) {
|
||||
// unlock the wallet and then finish the splash screen
|
||||
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
|
||||
return;
|
||||
}
|
||||
|
||||
this.finishSplashScreen();
|
||||
});
|
||||
this.finishSplashScreen();
|
||||
});
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
selectFirstRunCompleted,
|
||||
selectShowSuggestedSubs
|
||||
} from 'lbryinc';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import Constants from 'constants';
|
||||
|
@ -37,6 +37,7 @@ const perform = dispatch => ({
|
|||
doSetViewMode: (viewMode) => dispatch(doSetViewMode(viewMode)),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_SUBSCRIPTIONS)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SubscriptionsPage);
|
||||
|
|
|
@ -34,9 +34,11 @@ class SubscriptionsPage extends React.PureComponent {
|
|||
doFetchMySubscriptions,
|
||||
doFetchRecommendedSubscriptions,
|
||||
pushDrawerStack,
|
||||
setPlayerVisible
|
||||
} = this.props;
|
||||
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
doFetchMySubscriptions();
|
||||
doFetchRecommendedSubscriptions();
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import {
|
|||
selectTransactionItems,
|
||||
selectIsFetchingTransactions,
|
||||
} from 'lbry-redux';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import Constants from 'constants';
|
||||
import TransactionHistoryPage from './view';
|
||||
|
||||
|
@ -16,6 +16,7 @@ const select = state => ({
|
|||
const perform = dispatch => ({
|
||||
fetchTransactions: () => dispatch(doFetchTransactions()),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRANSACTION_HISTORY)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TransactionHistoryPage);
|
||||
|
|
|
@ -6,7 +6,9 @@ import walletStyle from 'styles/wallet';
|
|||
|
||||
class TransactionHistoryPage extends React.PureComponent {
|
||||
componentWillMount() {
|
||||
this.props.pushDrawerStack();
|
||||
const { pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doFetchTrendingUris, selectTrendingUris, selectFetchingTrendingUris } from 'lbryinc';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import Constants from 'constants';
|
||||
import TrendingPage from './view';
|
||||
|
||||
|
@ -11,7 +11,8 @@ const select = state => ({
|
|||
|
||||
const perform = dispatch => ({
|
||||
fetchTrendingUris: () => dispatch(doFetchTrendingUris()),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRENDING))
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_TRENDING)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(TrendingPage);
|
|
@ -19,8 +19,9 @@ import UriBar from 'component/uriBar';
|
|||
|
||||
class TrendingPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
const { fetchTrendingUris, pushDrawerStack } = this.props;
|
||||
const { fetchTrendingUris, pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
fetchTrendingUris();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doToast } from 'lbry-redux';
|
||||
import {
|
||||
doCheckSync,
|
||||
doGetSync,
|
||||
doSyncApply,
|
||||
doUserEmailNew,
|
||||
doUserEmailToVerify,
|
||||
doUserResendVerificationEmail,
|
||||
|
@ -14,8 +17,18 @@ import {
|
|||
selectEmailNewErrorMessage,
|
||||
selectEmailNewIsPending,
|
||||
selectEmailToVerify,
|
||||
selectHasSyncedWallet,
|
||||
selectGetSyncIsPending,
|
||||
selectSetSyncIsPending,
|
||||
selectSyncApplyIsPending,
|
||||
selectSyncApplyErrorMessage,
|
||||
selectSyncData,
|
||||
selectSyncHash,
|
||||
selectUser,
|
||||
} from 'lbryinc';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import Constants from 'constants';
|
||||
import Verification from './view';
|
||||
|
||||
const select = (state) => ({
|
||||
|
@ -28,15 +41,27 @@ const select = (state) => ({
|
|||
phone: selectPhoneToVerify(state),
|
||||
phoneNewErrorMessage: selectPhoneNewErrorMessage(state),
|
||||
phoneNewIsPending: selectPhoneNewIsPending(state),
|
||||
deviceWalletSynced: makeSelectClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED)(state),
|
||||
hasSyncedWallet: selectHasSyncedWallet(state),
|
||||
getSyncIsPending: selectGetSyncIsPending(state),
|
||||
setSyncIsPending: selectSetSyncIsPending(state),
|
||||
syncApplyIsPending: selectSyncApplyIsPending(state),
|
||||
syncApplyErrorMessage: selectSyncApplyErrorMessage(state),
|
||||
syncData: selectSyncData(state),
|
||||
syncHash: selectSyncHash(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
addUserEmail: email => dispatch(doUserEmailNew(email)),
|
||||
addUserPhone: (phone, country_code) => dispatch(doUserPhoneNew(phone, country_code)),
|
||||
getSync: password => dispatch(doGetSync(password)),
|
||||
checkSync: () => dispatch(doCheckSync()),
|
||||
verifyPhone: (verificationCode) => dispatch(doUserPhoneVerify(verificationCode)),
|
||||
notify: data => dispatch(doToast(data)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
|
||||
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email))
|
||||
syncApply: (hash, data, password) => dispatch(doSyncApply(hash, data, password)),
|
||||
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(Verification);
|
||||
|
|
|
@ -142,13 +142,15 @@ class EmailVerifyPage extends React.PureComponent {
|
|||
text={"Send verification email"}
|
||||
onPress={this.onSendVerificationPressed} />}
|
||||
{this.state.verifyStarted && emailNewPending &&
|
||||
<ActivityIndicator size={"small"} color={Colors.White} style={rewardStyle.loading} />}
|
||||
<View style={firstRunStyle.centerInside}>
|
||||
<ActivityIndicator size={"small"} color={Colors.White} />
|
||||
</View>}
|
||||
</View>
|
||||
</View>}
|
||||
|
||||
{(Constants.PHASE_VERIFICATION === this.state.phase) &&
|
||||
<View>
|
||||
<Text style={firstRunStyle.paragraph}>An email has been sent to {this.state.email}. Please follow the instructions in the message to verify your email address.</Text>
|
||||
<Text style={firstRunStyle.paragraph}>An email has been sent to <Text style={firstRunStyle.nowrap}>{this.state.email}</Text>. Please follow the instructions in the message to verify your email address.</Text>
|
||||
|
||||
<View style={rewardStyle.buttonContainer}>
|
||||
<Button style={rewardStyle.verificationButton} theme={"light"} text={"Resend"} onPress={this.onResendPressed} />
|
||||
|
|
|
@ -21,7 +21,7 @@ class ManualVerifyPage extends React.PureComponent {
|
|||
return (
|
||||
<View style={firstRunStyle.container}>
|
||||
<Text style={rewardStyle.verificationTitle}>Manual Reward Verification</Text>
|
||||
<Text style={firstRunStyle.paragraph}>You need to be manually verified before you can start claiming rewards. Please request to be verified on the <Link style={rewardStyle.underlinedTextLink} href="https://discordapp.com/invite/Z3bERWA" text="LBRY Discord server" />.</Text>
|
||||
<Text style={firstRunStyle.spacedParagraph}>You need to be manually verified before you can start claiming rewards. Please request to be verified on the <Link style={rewardStyle.underlinedTextLink} href="https://discordapp.com/invite/Z3bERWA" text="LBRY Discord server" />.</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -171,10 +171,12 @@ class PhoneVerifyPage extends React.PureComponent {
|
|||
text={"Send verification text"}
|
||||
onPress={this.onSendTextPressed} />}
|
||||
{phoneNewIsPending &&
|
||||
<ActivityIndicator
|
||||
style={[rewardStyle.loading, rewardStyle.topMarginMedium]}
|
||||
size="small"
|
||||
color={Colors.White} />}
|
||||
<View style={firstRunStyle.centerInside}>
|
||||
<ActivityIndicator
|
||||
style={rewardStyle.topMarginMedium}
|
||||
size="small"
|
||||
color={Colors.White} />
|
||||
</View>}
|
||||
</View>
|
||||
</View>}
|
||||
|
||||
|
@ -204,12 +206,12 @@ class PhoneVerifyPage extends React.PureComponent {
|
|||
</View>
|
||||
}
|
||||
{phoneVerifyIsPending &&
|
||||
<View>
|
||||
<View style={firstRunStyle.centered}>
|
||||
<Text style={firstRunStyle.paragraph}>Verifying your phone number...</Text>
|
||||
<ActivityIndicator
|
||||
color={Colors.White}
|
||||
size="small"
|
||||
style={[rewardStyle.loading, rewardStyle.topMarginMedium, rewardStyle.leftRightMargin]} />
|
||||
style={[rewardStyle.topMarginMedium, rewardStyle.leftRightMargin]} />
|
||||
</View>}
|
||||
</View>
|
||||
}
|
||||
|
|
170
app/src/page/verification/internal/sync-verify-page.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
import React from 'react';
|
||||
import { Lbry } from 'lbry-redux';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
NativeModules,
|
||||
Text,
|
||||
TextInput,
|
||||
View
|
||||
} from 'react-native';
|
||||
import { BarPasswordStrengthDisplay } from 'react-native-password-strength-meter';
|
||||
import Button from 'component/button';
|
||||
import Link from 'component/link';
|
||||
import Colors from 'styles/colors';
|
||||
import Constants from 'constants';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
import rewardStyle from 'styles/reward';
|
||||
|
||||
|
||||
class SyncVerifyPage extends React.PureComponent {
|
||||
state = {
|
||||
checkSyncStarted: false,
|
||||
password: null,
|
||||
placeholder: 'password',
|
||||
syncApplyStarted: false,
|
||||
syncChecked: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { checkSync, setEmailVerificationPhase } = this.props;
|
||||
|
||||
this.setState({ checkSyncStarted: true }, () => checkSync());
|
||||
|
||||
if (setEmailVerificationPhase) {
|
||||
setEmailVerificationPhase(false);
|
||||
}
|
||||
}
|
||||
|
||||
onEnableSyncPressed = () => {
|
||||
const {
|
||||
getSync,
|
||||
hasSyncedWallet,
|
||||
navigation,
|
||||
setClientSetting,
|
||||
syncApply,
|
||||
syncData,
|
||||
syncHash
|
||||
} = this.props;
|
||||
|
||||
this.setState({ syncApplyStarted: true }, () => {
|
||||
if (!hasSyncedWallet) {
|
||||
// fresh account with no sync
|
||||
Lbry.account_encrypt({ new_password: this.state.password }).then(() => {
|
||||
Lbry.account_unlock({ password: this.state.password }).then(() => {
|
||||
getSync(this.state.password);
|
||||
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
|
||||
navigation.goBack();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
syncApply(syncHash, syncData, this.state.password);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { getSyncIsPending, syncApplyIsPending, syncApplyErrorMessage } = nextProps;
|
||||
const { getSync, setClientSetting, navigation, notify, hasSyncedWallet } = this.props;
|
||||
if (this.state.checkSyncStarted && !getSyncIsPending) {
|
||||
this.setState({ syncChecked: true });
|
||||
}
|
||||
|
||||
if (this.state.syncApplyStarted && !syncApplyIsPending) {
|
||||
if (syncApplyErrorMessage && syncApplyErrorMessage.trim().length > 0) {
|
||||
notify({ message: syncApplyErrorMessage, isError: true });
|
||||
this.setState({ syncApplyStarted: false });
|
||||
} else {
|
||||
// password successfully verified
|
||||
if (NativeModules.UtilityModule) {
|
||||
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, this.state.password);
|
||||
}
|
||||
setClientSetting(Constants.SETTING_DEVICE_WALLET_SYNCED, true);
|
||||
navigation.goBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleChangeText = (text) => {
|
||||
// save the value to the state email
|
||||
const { onPasswordChanged } = this.props;
|
||||
this.setState({ password: text });
|
||||
if (onPasswordChanged) {
|
||||
onPasswordChanged(text);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hasSyncedWallet, syncApplyIsPending } = this.props;
|
||||
|
||||
let paragraph;
|
||||
if (!hasSyncedWallet) {
|
||||
paragraph = (<Text style={firstRunStyle.paragraph}>Please enter a password to secure your account and wallet.</Text>);
|
||||
} else {
|
||||
paragraph = (<Text style={firstRunStyle.paragraph}>Please enter the password you used to secure your wallet.</Text>);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (!this.state.syncChecked) {
|
||||
content = (
|
||||
<View style={firstRunStyle.centered}>
|
||||
<ActivityIndicator size="large" color={Colors.White} style={firstRunStyle.waiting} />
|
||||
<Text style={firstRunStyle.paragraph}>Retrieving your account information...</Text>
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<View>
|
||||
<Text style={rewardStyle.verificationTitle}>Wallet Sync</Text>
|
||||
{paragraph}
|
||||
<TextInput style={firstRunStyle.passwordInput}
|
||||
placeholder={this.state.placeholder}
|
||||
underlineColorAndroid="transparent"
|
||||
secureTextEntry={true}
|
||||
value={this.state.password}
|
||||
onChangeText={text => this.handleChangeText(text)}
|
||||
onFocus={() => {
|
||||
if (!this.state.password || this.state.password.length === 0) {
|
||||
this.setState({ placeholder: '' });
|
||||
}
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!this.state.password || this.state.password.length === 0) {
|
||||
this.setState({ placeholder: 'password' });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{(!hasSyncedWallet && this.state.password && this.state.password.trim().length) > 0 &&
|
||||
<View style={firstRunStyle.passwordStrength}>
|
||||
<BarPasswordStrengthDisplay
|
||||
width={Dimensions.get('window').width - 80}
|
||||
minLength={1}
|
||||
password={this.state.password} />
|
||||
</View>}
|
||||
<Text style={firstRunStyle.infoParagraph}>Note: for wallet security purposes, LBRY is unable to reset your password.</Text>
|
||||
|
||||
<View style={rewardStyle.buttonContainer}>
|
||||
{!this.state.syncApplyStarted &&
|
||||
<Button
|
||||
style={rewardStyle.verificationButton}
|
||||
theme={"light"}
|
||||
text={"Enable sync"}
|
||||
onPress={this.onEnableSyncPressed} />}
|
||||
{syncApplyIsPending &&
|
||||
<View style={firstRunStyle.centerInside}>
|
||||
<ActivityIndicator size={"small"} color={Colors.White} />
|
||||
</View>}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={firstRunStyle.container}>
|
||||
{content}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default SyncVerifyPage;
|
|
@ -15,6 +15,7 @@ import Constants from 'constants';
|
|||
import EmailVerifyPage from './internal/email-verify-page';
|
||||
import ManualVerifyPage from './internal/manual-verify-page';
|
||||
import PhoneVerifyPage from './internal/phone-verify-page';
|
||||
import SyncVerifyPage from './internal/sync-verify-page';
|
||||
import firstRunStyle from 'styles/firstRun';
|
||||
|
||||
class VerificationScreen extends React.PureComponent {
|
||||
|
@ -43,7 +44,8 @@ class VerificationScreen extends React.PureComponent {
|
|||
}
|
||||
|
||||
checkVerificationStatus = (user) => {
|
||||
const { navigation } = this.props;
|
||||
const { deviceWalletSynced, navigation } = this.props;
|
||||
const { syncFlow } = navigation.state.params;
|
||||
|
||||
this.setState({
|
||||
isEmailVerified: (user && user.primary_email && user.has_verified_email),
|
||||
|
@ -53,11 +55,23 @@ class VerificationScreen extends React.PureComponent {
|
|||
if (!this.state.isEmailVerified) {
|
||||
this.setState({ currentPage: 'emailVerify' });
|
||||
}
|
||||
if (this.state.isEmailVerified && !this.state.isIdentityVerified) {
|
||||
this.setState({ currentPage: 'phoneVerify' });
|
||||
|
||||
if (syncFlow) {
|
||||
if (this.state.isEmailVerified && !deviceWalletSynced) {
|
||||
this.setState({ currentPage: 'syncVerify' });
|
||||
}
|
||||
} else {
|
||||
if (this.state.isEmailVerified && !this.state.isIdentityVerified) {
|
||||
this.setState({ currentPage: 'phoneVerify' });
|
||||
}
|
||||
if (this.state.isEmailVerified && this.state.isIdentityVerified && !this.state.isRewardApproved) {
|
||||
this.setState({ currentPage: 'manualVerify' });
|
||||
}
|
||||
}
|
||||
if (this.state.isEmailVerified && this.state.isIdentityVerified && !this.state.isRewardApproved) {
|
||||
this.setState({ currentPage: 'manualVerify' });
|
||||
|
||||
if (this.state.isEmailVerified && syncFlow && deviceWalletSynced) {
|
||||
navigation.goBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.isEmailVerified && this.state.isIdentityVerified && this.state.isRewardApproved) {
|
||||
|
@ -81,18 +95,29 @@ class VerificationScreen extends React.PureComponent {
|
|||
render() {
|
||||
const {
|
||||
addUserEmail,
|
||||
checkSync,
|
||||
emailNewErrorMessage,
|
||||
emailNewPending,
|
||||
emailToVerify,
|
||||
getSync,
|
||||
navigation,
|
||||
notify,
|
||||
addUserPhone,
|
||||
getSyncIsPending,
|
||||
hasSyncedWallet,
|
||||
setSyncIsPending,
|
||||
syncApplyIsPending,
|
||||
syncApplyErrorMessage,
|
||||
syncApply,
|
||||
syncData,
|
||||
syncHash,
|
||||
phone,
|
||||
phoneVerifyIsPending,
|
||||
phoneVerifyErrorMessage,
|
||||
phoneNewIsPending,
|
||||
phoneNewErrorMessage,
|
||||
resendVerificationEmail,
|
||||
setClientSetting,
|
||||
verifyPhone
|
||||
} = this.props;
|
||||
|
||||
|
@ -111,6 +136,7 @@ class VerificationScreen extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'phoneVerify':
|
||||
page = (
|
||||
<PhoneVerifyPage
|
||||
|
@ -126,10 +152,33 @@ class VerificationScreen extends React.PureComponent {
|
|||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'syncVerify':
|
||||
page = (
|
||||
<SyncVerifyPage
|
||||
checkSync={checkSync}
|
||||
getSync={getSync}
|
||||
getSyncIsPending={getSyncIsPending}
|
||||
hasSyncedWallet={hasSyncedWallet}
|
||||
navigation={navigation}
|
||||
notify={notify}
|
||||
setEmailVerificationPhase={this.setEmailVerificationPhase}
|
||||
setClientSetting={setClientSetting}
|
||||
setSyncIsPending={setSyncIsPending}
|
||||
syncApplyIsPending={syncApplyIsPending}
|
||||
syncApplyErrorMessage={syncApplyErrorMessage}
|
||||
syncApply={syncApply}
|
||||
syncData={syncData}
|
||||
syncHash={syncHash}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
|
||||
case 'manualVerify':
|
||||
page = (
|
||||
<ManualVerifyPage setEmailVerificationPhase={this.setEmailVerificationPhase} />
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { selectBalance } from 'lbry-redux';
|
||||
import { doGetSync, selectUser } from 'lbryinc';
|
||||
import { doCheckSync, doGetSync, selectUser, selectHasSyncedWallet } from 'lbryinc';
|
||||
import Constants from 'constants';
|
||||
import WalletPage from './view';
|
||||
|
||||
const select = state => ({
|
||||
user: selectUser(state),
|
||||
balance: selectBalance(state),
|
||||
hasSyncedWallet: selectHasSyncedWallet(state),
|
||||
understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state),
|
||||
backupDismissed: makeSelectClientSetting(Constants.SETTING_BACKUP_DISMISSED)(state),
|
||||
rewardsNotInterested: makeSelectClientSetting(Constants.SETTING_REWARDS_NOT_INTERESTED)(state),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
checkSync: () => dispatch(doCheckSync()),
|
||||
getSync: password => dispatch(doGetSync(password)),
|
||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET))
|
||||
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET)),
|
||||
setPlayerVisible: () => dispatch(doSetPlayerVisible(false))
|
||||
});
|
||||
|
||||
export default connect(select, perform)(WalletPage);
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { NativeModules, ScrollView, Text, View } from 'react-native';
|
||||
import TransactionListRecent from 'component/transactionListRecent';
|
||||
import WalletRewardsDriver from 'component/walletRewardsDriver';
|
||||
import WalletAddress from 'component/walletAddress';
|
||||
import WalletBalance from 'component/walletBalance';
|
||||
import WalletSend from 'component/walletSend';
|
||||
import WalletRewardsDriver from 'component/walletRewardsDriver';
|
||||
import WalletSyncDriver from 'component/walletSyncDriver';
|
||||
import Button from 'component/button';
|
||||
import Link from 'component/link';
|
||||
import UriBar from 'component/uriBar';
|
||||
|
@ -13,11 +14,17 @@ import walletStyle from 'styles/wallet';
|
|||
|
||||
class WalletPage extends React.PureComponent {
|
||||
componentDidMount() {
|
||||
this.props.pushDrawerStack();
|
||||
const { pushDrawerStack, setPlayerVisible } = this.props;
|
||||
pushDrawerStack();
|
||||
setPlayerVisible();
|
||||
|
||||
const { user, getSync } = this.props;
|
||||
const { getSync, user } = this.props;
|
||||
if (user && user.has_verified_email) {
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => getSync(walletPassword));
|
||||
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
|
||||
if (walletPassword && walletPassword.trim().length > 0) {
|
||||
getSync(walletPassword);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -30,6 +37,7 @@ class WalletPage extends React.PureComponent {
|
|||
const {
|
||||
balance,
|
||||
backupDismissed,
|
||||
hasSyncedWallet,
|
||||
rewardsNotInterested,
|
||||
understandsRisks,
|
||||
setClientSetting,
|
||||
|
@ -41,8 +49,15 @@ class WalletPage extends React.PureComponent {
|
|||
<View>
|
||||
<UriBar navigation={navigation} />
|
||||
<View style={walletStyle.warning}>
|
||||
<Text style={walletStyle.warningParagraph}>
|
||||
This is beta software. You may lose any credits that you send to your wallet due to software bugs, deleted files, or malicious third-party software. You should not use this wallet as your primary wallet.
|
||||
</Text>
|
||||
{!hasSyncedWallet &&
|
||||
<Text style={walletStyle.warningParagraph}>
|
||||
If you are not using the LBRY sync service, you will lose all of your credits if you uninstall this application. Instructions on how to enroll as well as how to backup your wallet manually are available on the next page.
|
||||
</Text>}
|
||||
<Text style={walletStyle.warningText}>
|
||||
This is beta software. You may lose any LBC that you send to your wallet due to uninstallation, software bugs, deleted files, or malicious third-party software. You should not use this wallet as your primary wallet. If you understand the risks and you wish to continue, please tap the button below.
|
||||
If you understand the risks and you wish to continue, please tap the button below.
|
||||
</Text>
|
||||
</View>
|
||||
<Button text={'I understand the risks'} style={[walletStyle.button, walletStyle.understand]}
|
||||
|
@ -55,14 +70,7 @@ class WalletPage extends React.PureComponent {
|
|||
<View style={walletStyle.container}>
|
||||
<UriBar navigation={navigation} />
|
||||
<ScrollView style={walletStyle.scrollContainer} keyboardShouldPersistTaps={'handled'}>
|
||||
{!backupDismissed &&
|
||||
<View style={walletStyle.warningCard}>
|
||||
<Text style={walletStyle.warningText}>
|
||||
Please backup your wallet file using the instructions at <Link style={walletStyle.warningText} text="https://lbry.com/faq/how-to-backup-wallet#android" href="https://lbry.com/faq/how-to-backup-wallet#android" />.
|
||||
</Text>
|
||||
<Button text={'Dismiss'} style={walletStyle.button} onPress={this.onDismissBackupPressed} />
|
||||
</View>}
|
||||
|
||||
<WalletSyncDriver navigation={navigation} />
|
||||
{(!rewardsNotInterested) && (!balance || balance === 0) && <WalletRewardsDriver navigation={navigation} />}
|
||||
<WalletBalance />
|
||||
<WalletAddress />
|
||||
|
|
|
@ -8,3 +8,8 @@ export const doPushDrawerStack = (routeName) => (dispatch) => dispatch({
|
|||
export const doPopDrawerStack = () => (dispatch) => dispatch({
|
||||
type: Constants.ACTION_POP_DRAWER_STACK
|
||||
});
|
||||
|
||||
export const doSetPlayerVisible = (visible) => (dispatch) => dispatch({
|
||||
type: Constants.ACTION_SET_PLAYER_VISIBLE,
|
||||
data: { visible }
|
||||
});
|
||||
|
|
|
@ -1,112 +1,8 @@
|
|||
import {
|
||||
ACTIONS,
|
||||
Lbry,
|
||||
doToast,
|
||||
formatCredits,
|
||||
selectBalance,
|
||||
makeSelectFileInfoForUri,
|
||||
makeSelectMetadataForUri,
|
||||
selectDownloadingByOutpoint,
|
||||
} from 'lbry-redux';
|
||||
import { doClaimEligiblePurchaseRewards, makeSelectCostInfoForUri } from 'lbryinc';
|
||||
import { ACTIONS, Lbry } from 'lbry-redux';
|
||||
import { doClaimEligiblePurchaseRewards } from 'lbryinc';
|
||||
import { Alert, NativeModules } from 'react-native';
|
||||
import Constants from 'constants';
|
||||
|
||||
const DOWNLOAD_POLL_INTERVAL = 250;
|
||||
|
||||
const deleteBlobsForSdHash = (sdHash) => {
|
||||
Lbry.blob_list({ sd_hash: sdHash }).then(hashes => {
|
||||
hashes.filter(hash => hash != sdHash).forEach(hash => {
|
||||
Lbry.blob_delete({ blob_hash: hash });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function doUpdateLoadStatus(uri, outpoint) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
Lbry.file_list({
|
||||
outpoint,
|
||||
full_status: true,
|
||||
}).then(([fileInfo]) => {
|
||||
if (!fileInfo || fileInfo.written_bytes === 0) {
|
||||
// if the outpoint isn't in the state, then it was probably canceled, so stop checking the load status
|
||||
const { downloadingByOutpoint = {} } = state.fileInfo;
|
||||
if (!downloadingByOutpoint[outpoint]) return;
|
||||
|
||||
// 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,
|
||||
},
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.updateDownload(
|
||||
uri,
|
||||
fileInfo.file_name ? fileInfo.file_name : '',
|
||||
100,
|
||||
writtenBytes ? writtenBytes : 0,
|
||||
totalBytes ? totalBytes : 0
|
||||
);
|
||||
}
|
||||
|
||||
// Once a download has been completed, delete the individual blob files to save space
|
||||
Lbry.file_set_status({ status: 'stop', sd_hash: fileInfo.sd_hash }).then(() => {
|
||||
deleteBlobsForSdHash(fileInfo.sd_hash);
|
||||
}).catch(() => {
|
||||
deleteBlobsForSdHash(fileInfo.sd_hash);
|
||||
});
|
||||
|
||||
/*const notif = new window.Notification('LBRY Download Complete', {
|
||||
body: fileInfo.metadata.stream.metadata.title,
|
||||
silent: false,
|
||||
});*/
|
||||
} 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,
|
||||
},
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.updateDownload(
|
||||
uri,
|
||||
fileInfo.file_name ? fileInfo.file_name : '',
|
||||
progress ? progress : 0,
|
||||
writtenBytes ? writtenBytes : 0,
|
||||
totalBytes ? totalBytes: 0
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
}, DOWNLOAD_POLL_INTERVAL);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doStartDownload(uri, outpoint) {
|
||||
export function doStartDownload(uri, outpoint, fileInfo) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
|
||||
|
@ -118,22 +14,45 @@ export function doStartDownload(uri, outpoint) {
|
|||
|
||||
if (downloadingByOutpoint[outpoint]) return;
|
||||
|
||||
Lbry.file_list({ outpoint, full_status: true }).then(([fileInfo]) => {
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_STARTED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
dispatch(doClaimEligiblePurchaseRewards());
|
||||
};
|
||||
}
|
||||
|
||||
export function doUpdateDownload(uri, outpoint, fileInfo, progress) {
|
||||
return (dispatch) => {
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_PROGRESSED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
progress,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doCompleteDownload(uri, outpoint, fileInfo) {
|
||||
return (dispatch) => {
|
||||
if (fileInfo.completed) {
|
||||
dispatch({
|
||||
type: ACTIONS.DOWNLOADING_STARTED,
|
||||
type: ACTIONS.DOWNLOADING_COMPLETED,
|
||||
data: {
|
||||
uri,
|
||||
outpoint,
|
||||
fileInfo,
|
||||
},
|
||||
});
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.startDownload(uri, fileInfo.file_name ? fileInfo.file_name : '');
|
||||
}
|
||||
|
||||
dispatch(doUpdateLoadStatus(uri, outpoint));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -155,22 +74,10 @@ export function doStopDownloadingFile(uri, fileInfo) {
|
|||
|
||||
// Should also delete the file after the user stops downloading
|
||||
dispatch(doDeleteFile(fileInfo.outpoint, uri));
|
||||
|
||||
if (NativeModules.LbryDownloadManager) {
|
||||
NativeModules.LbryDownloadManager.stopDownload(uri, fileInfo.file_name ? fileInfo.file_name : '');
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doDownloadFile(uri, streamInfo) {
|
||||
return dispatch => {
|
||||
const { outpoint } = streamInfo;
|
||||
dispatch(doStartDownload(uri, outpoint));
|
||||
dispatch(doClaimEligiblePurchaseRewards());
|
||||
};
|
||||
}
|
||||
|
||||
export function doSetPlayingUri(uri) {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
|
@ -180,133 +87,6 @@ export function doSetPlayingUri(uri) {
|
|||
};
|
||||
}
|
||||
|
||||
export function doLoadVideo(uri, failureCallback) {
|
||||
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 },
|
||||
});
|
||||
|
||||
dispatch(doToast({
|
||||
message: `File timeout for uri ${uri}`,
|
||||
}));
|
||||
|
||||
if (failureCallback) {
|
||||
failureCallback();
|
||||
}
|
||||
} else {
|
||||
dispatch(doDownloadFile(uri, streamInfo));
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
dispatch(doSetPlayingUri(null));
|
||||
dispatch({
|
||||
type: ACTIONS.LOADING_VIDEO_FAILED,
|
||||
data: { uri },
|
||||
});
|
||||
|
||||
dispatch(doToast({
|
||||
message: `Failed to download ${uri}, please try again. If this problem persists, visit https://lbry.com/faq/support for support.`,
|
||||
}));
|
||||
|
||||
if (failureCallback) {
|
||||
failureCallback();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function doPurchaseUri(uri, specificCostInfo, failureCallback) {
|
||||
return (dispatch, getState) => {
|
||||
const state = getState();
|
||||
const balance = selectBalance(state);
|
||||
const fileInfo = makeSelectFileInfoForUri(uri)(state);
|
||||
const metadata = makeSelectMetadataForUri(uri)(state);
|
||||
const title = metadata ? metadata.title : uri;
|
||||
const downloadingByOutpoint = selectDownloadingByOutpoint(state);
|
||||
const alreadyDownloading = fileInfo && !!downloadingByOutpoint[fileInfo.outpoint];
|
||||
|
||||
function attemptPlay(cost, instantPurchaseMax = null) {
|
||||
if (cost > 0 && (!instantPurchaseMax || cost > instantPurchaseMax)) {
|
||||
// display alert
|
||||
const formattedCost = formatCredits(cost, 2);
|
||||
const unit = cost === 1 ? 'credit' : 'credits';
|
||||
Alert.alert('Confirm purchase',
|
||||
`This will purchase "${title}" for ${formattedCost} ${unit}`,
|
||||
[
|
||||
{ text: 'OK', onPress: () => dispatch(doLoadVideo(uri)) },
|
||||
{ text: 'Cancel', style: 'cancel', onPress: () => failureCallback && failureCallback() }
|
||||
],
|
||||
{ cancelable: true });
|
||||
} else {
|
||||
dispatch(doLoadVideo(uri, failureCallback));
|
||||
}
|
||||
}
|
||||
|
||||
// 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(doToast({
|
||||
message: 'Insufficient credits',
|
||||
}));
|
||||
if (failureCallback) {
|
||||
failureCallback();
|
||||
}
|
||||
|
||||
Promise.resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
attemptPlay(cost);
|
||||
/*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);
|
||||
});
|
||||
}
|
||||
}*/
|
||||
};
|
||||
}
|
||||
|
||||
export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
||||
return (dispatch, getState) => {
|
||||
Lbry.file_delete({
|
||||
|
@ -334,29 +114,5 @@ export function doDeleteFile(outpoint, deleteFromComputer, abandonClaim) {
|
|||
outpoint,
|
||||
},
|
||||
});
|
||||
|
||||
//const totalProgress = selectTotalDownloadProgress(getState());
|
||||
//setProgressBar(totalProgress);
|
||||
};
|
||||
}
|
||||
|
||||
export function doDeleteCompleteBlobs() {
|
||||
return dispatch => {
|
||||
dispatch({
|
||||
type: Constants.ACTION_DELETE_COMPLETED_BLOBS,
|
||||
data: {},
|
||||
});
|
||||
|
||||
Lbry.file_list().then(files => {
|
||||
files.forEach(fileInfo => {
|
||||
if (fileInfo.completed) {
|
||||
Lbry.file_set_status({ status: 'stop', sd_hash: fileInfo.sd_hash }).then(() => {
|
||||
deleteBlobsForSdHash(fileInfo.sd_hash);
|
||||
}).catch(() => {
|
||||
deleteBlobsForSdHash(fileInfo.sd_hash);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -2,9 +2,15 @@ import Constants from 'constants';
|
|||
|
||||
const reducers = {};
|
||||
const defaultState = {
|
||||
stack: [ Constants.DRAWER_ROUTE_DISCOVER ] // Discover is always the first drawer route
|
||||
stack: [ Constants.DRAWER_ROUTE_DISCOVER ], // Discover is always the first drawer route
|
||||
playerVisible: false
|
||||
};
|
||||
|
||||
reducers[Constants.ACTION_SET_PLAYER_VISIBLE] = (state, action) =>
|
||||
Object.assign({}, state, {
|
||||
playerVisible: action.data.visible
|
||||
});
|
||||
|
||||
reducers[Constants.ACTION_PUSH_DRAWER_STACK] = (state, action) => {
|
||||
const routeName = action.data;
|
||||
const newStack = state.stack.slice();
|
||||
|
|
|
@ -2,9 +2,11 @@ import { createSelector } from 'reselect';
|
|||
|
||||
export const selectState = state => state.drawer || {};
|
||||
|
||||
export const selectDrawerStack = createSelector(selectState, (state) => state.stack);
|
||||
export const selectDrawerStack = createSelector(selectState, state => state.stack);
|
||||
|
||||
export const selectLastDrawerRoute = createSelector(selectState, (state) => {
|
||||
export const selectIsPlayerVisible = createSelector(selectState, state => state.playerVisible);
|
||||
|
||||
export const selectLastDrawerRoute = createSelector(selectState, state => {
|
||||
if (state.stack.length) {
|
||||
return state.stack[state.stack.length - 1];
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ const Colors = {
|
|||
DarkGrey: '#555555',
|
||||
DescriptionGrey: '#999999',
|
||||
LbryGreen: '#2f9176',
|
||||
BrightGreen: '#61fcd8',
|
||||
BrighterLbryGreen: '#40b887',
|
||||
NextLbryGreen: '#38d9a9',
|
||||
LightGrey: '#cccccc',
|
||||
LighterGrey: '#e5e5e5',
|
||||
Orange: '#ffbb00',
|
||||
|
|
|
@ -77,7 +77,7 @@ const discoverStyle = StyleSheet.create({
|
|||
top: 8
|
||||
},
|
||||
filePriceContainer: {
|
||||
backgroundColor: Colors.BrightGreen,
|
||||
backgroundColor: Colors.NextLbryGreen,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { StyleSheet } from 'react-native';
|
||||
import Colors from 'styles/colors';
|
||||
|
||||
const fileDownloadButtonStyle = StyleSheet.create({
|
||||
container: {
|
||||
|
@ -7,11 +8,11 @@ const fileDownloadButtonStyle = StyleSheet.create({
|
|||
height: 36,
|
||||
borderRadius: 18,
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#40c0a9',
|
||||
backgroundColor: Colors.LbryGreen,
|
||||
},
|
||||
text: {
|
||||
fontFamily: 'Inter-UI-Medium',
|
||||
color: '#ffffff',
|
||||
color: Colors.White,
|
||||
fontSize: 14,
|
||||
textAlign: 'center'
|
||||
}
|
||||
|
|
|
@ -58,6 +58,9 @@ const filePageStyle = StyleSheet.create({
|
|||
fontSize: 16,
|
||||
flex: 18
|
||||
},
|
||||
titleTouch: {
|
||||
flex: 1,
|
||||
},
|
||||
titleRow: {
|
||||
flexDirection: 'row',
|
||||
marginTop: 12,
|
||||
|
@ -159,7 +162,7 @@ const filePageStyle = StyleSheet.create({
|
|||
bottom: 0
|
||||
},
|
||||
filePriceContainer: {
|
||||
backgroundColor: '#61fcd8',
|
||||
backgroundColor: Colors.NextLbryGreen,
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
|
@ -239,8 +242,8 @@ const filePageStyle = StyleSheet.create({
|
|||
flex: 1,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 60,
|
||||
top: 60,
|
||||
bottom: 0,
|
||||
zIndex: 100
|
||||
},
|
||||
link: {
|
||||
|
@ -286,7 +289,9 @@ const filePageStyle = StyleSheet.create({
|
|||
},
|
||||
currency: {
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: 17
|
||||
fontSize: 12,
|
||||
marginTop: 15,
|
||||
marginLeft: 4
|
||||
},
|
||||
descriptionToggle: {
|
||||
alignItems: 'center',
|
||||
|
@ -320,6 +325,36 @@ const filePageStyle = StyleSheet.create({
|
|||
},
|
||||
tagItem: {
|
||||
marginRight: 16
|
||||
},
|
||||
rewardDriverCard: {
|
||||
alignItems: 'center',
|
||||
backgroundColor: Colors.BrighterLbryGreen,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12
|
||||
},
|
||||
rewardDriverText: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
color: Colors.White,
|
||||
fontSize: 14
|
||||
},
|
||||
rewardIcon: {
|
||||
color: Colors.White,
|
||||
marginRight: 8
|
||||
},
|
||||
backButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
backButtonIcon: {
|
||||
color: Colors.White
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -33,6 +33,15 @@ const firstRunStyle = StyleSheet.create({
|
|||
marginBottom: 20,
|
||||
color: Colors.White
|
||||
},
|
||||
spacedParagraph: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 18,
|
||||
lineHeight: 28,
|
||||
marginLeft: 32,
|
||||
marginRight: 32,
|
||||
marginBottom: 20,
|
||||
color: Colors.White
|
||||
},
|
||||
infoParagraph: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
|
@ -163,6 +172,13 @@ const firstRunStyle = StyleSheet.create({
|
|||
marginRight: 32,
|
||||
marginBottom: 48
|
||||
},
|
||||
centered: {
|
||||
alignItems: 'center'
|
||||
},
|
||||
centerInside: {
|
||||
flex: 1,
|
||||
alignItems:'center'
|
||||
},
|
||||
nowrap: {
|
||||
flex: 1,
|
||||
flexWrap: 'nowrap'
|
||||
|
|
|
@ -74,7 +74,7 @@ const mediaPlayerStyle = StyleSheet.create({
|
|||
height: 36,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
right: 0,
|
||||
right: 4,
|
||||
bottom: 14,
|
||||
},
|
||||
elapsedDuration: {
|
||||
|
@ -82,15 +82,15 @@ const mediaPlayerStyle = StyleSheet.create({
|
|||
position: 'absolute',
|
||||
left: 8,
|
||||
bottom: 24,
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
color: '#ffffff'
|
||||
},
|
||||
totalDuration: {
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
position: 'absolute',
|
||||
right: 40,
|
||||
right: 48,
|
||||
bottom: 24,
|
||||
fontSize: 14,
|
||||
fontSize: 12,
|
||||
color: '#ffffff'
|
||||
},
|
||||
seekerCircle: {
|
||||
|
@ -136,6 +136,27 @@ const mediaPlayerStyle = StyleSheet.create({
|
|||
height: 24,
|
||||
width: 24,
|
||||
backgroundColor: Colors.LbryGreen
|
||||
},
|
||||
loadingContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0
|
||||
},
|
||||
backButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: 48,
|
||||
height: 48,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
backButtonIcon: {
|
||||
color: Colors.White
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -29,6 +29,10 @@ const walletStyle = StyleSheet.create({
|
|||
backgroundColor: Colors.LbryGreen,
|
||||
alignSelf: 'flex-start'
|
||||
},
|
||||
enrollButton: {
|
||||
backgroundColor: Colors.White,
|
||||
alignSelf: 'flex-start'
|
||||
},
|
||||
historyList: {
|
||||
backgroundColor: '#ffffff'
|
||||
},
|
||||
|
@ -51,12 +55,12 @@ const walletStyle = StyleSheet.create({
|
|||
margin: 16
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Inter-UI-Bold',
|
||||
fontFamily: 'Inter-UI-SemiBold',
|
||||
fontSize: 20,
|
||||
marginBottom: 24
|
||||
},
|
||||
transactionsTitle: {
|
||||
fontFamily: 'Inter-UI-Bold',
|
||||
fontFamily: 'Inter-UI-SemiBold',
|
||||
fontSize: 20
|
||||
},
|
||||
transactionsHeader: {
|
||||
|
@ -93,8 +97,8 @@ const walletStyle = StyleSheet.create({
|
|||
},
|
||||
balanceTitle: {
|
||||
color: '#ffffff',
|
||||
fontFamily: 'Inter-UI-Bold',
|
||||
fontSize: 18,
|
||||
fontFamily: 'Inter-UI-SemiBold',
|
||||
fontSize: 20,
|
||||
marginLeft: 16,
|
||||
marginTop: 16
|
||||
},
|
||||
|
@ -141,6 +145,13 @@ const walletStyle = StyleSheet.create({
|
|||
margin: 16,
|
||||
marginTop: 76
|
||||
},
|
||||
warningParagraph: {
|
||||
color: Colors.White,
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 16,
|
||||
lineHeight: 24,
|
||||
marginBottom: 16
|
||||
},
|
||||
warningText: {
|
||||
color: Colors.White,
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
|
@ -156,7 +167,9 @@ const walletStyle = StyleSheet.create({
|
|||
},
|
||||
currency: {
|
||||
alignSelf: 'flex-start',
|
||||
marginTop: 17
|
||||
fontSize: 12,
|
||||
marginTop: 16,
|
||||
marginLeft: 4
|
||||
},
|
||||
sendButton: {
|
||||
marginTop: 8
|
||||
|
@ -185,6 +198,29 @@ const walletStyle = StyleSheet.create({
|
|||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
lineHeight: 16
|
||||
},
|
||||
syncDriverCard: {
|
||||
padding: 16,
|
||||
backgroundColor: Colors.LbryGreen,
|
||||
marginLeft: 16,
|
||||
marginTop: 16,
|
||||
marginRight: 16
|
||||
},
|
||||
syncDriverTitle: {
|
||||
color: Colors.White,
|
||||
fontFamily: 'Inter-UI-SemiBold',
|
||||
fontSize: 20
|
||||
},
|
||||
syncDriverText: {
|
||||
color: Colors.White,
|
||||
fontFamily: 'Inter-UI-Regular',
|
||||
fontSize: 14,
|
||||
},
|
||||
actionRow: {
|
||||
marginTop: 20,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between'
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NavigationActions, StackActions } from 'react-navigation';
|
||||
import { buildURI, isURIValid } from 'lbry-redux';
|
||||
import { doPopDrawerStack, doPushDrawerStack } from 'redux/actions/drawer';
|
||||
import { doPopDrawerStack, doPushDrawerStack, doSetPlayerVisible } from 'redux/actions/drawer';
|
||||
import { DrawerRoutes } from 'constants';
|
||||
import Constants from 'constants';
|
||||
|
||||
|
@ -36,6 +36,7 @@ export function dispatchNavigateToUri(dispatch, nav, uri, isNavigatingBack) {
|
|||
|
||||
if (!isNavigatingBack) {
|
||||
dispatch(doPushDrawerStack(uri));
|
||||
dispatch(doSetPlayerVisible(true));
|
||||
}
|
||||
|
||||
if (nav && nav.routes && nav.routes.length > 0 && 'Main' === nav.routes[0].routeName) {
|
||||
|
@ -120,6 +121,7 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
|
|||
navigation.dispatch(stackAction);
|
||||
if (store && store.dispatch && !isNavigatingBack) {
|
||||
store.dispatch(doPushDrawerStack(uri));
|
||||
store.dispatch(doSetPlayerVisible(true));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
@ -127,6 +129,7 @@ export function navigateToUri(navigation, uri, additionalParams, isNavigatingBac
|
|||
navigation.navigate({ routeName: 'File', key: uri, params });
|
||||
if (store && store.dispatch && !isNavigatingBack) {
|
||||
store.dispatch(doPushDrawerStack(uri));
|
||||
store.dispatch(doSetPlayerVisible(true));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
|
|||
|
||||
# (list) Application requirements
|
||||
# comma seperated e.g. requirements = sqlite3,kivy
|
||||
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.36.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
|
||||
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.37.2#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git@a404269d91cff5358bcffb8067b0fd1d9c6842d3#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba@397ebe842856a4297bea5281206f9fa40a84699c#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
|
||||
|
||||
# (str) Custom source folders for requirements
|
||||
# Sets custom source for any requirements with recipes
|
||||
|
|
|
@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
|
|||
|
||||
# (list) Application requirements
|
||||
# comma seperated e.g. requirements = sqlite3,kivy
|
||||
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.36.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
|
||||
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.37.2#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git@a404269d91cff5358bcffb8067b0fd1d9c6842d3#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba@397ebe842856a4297bea5281206f9fa40a84699c#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
|
||||
|
||||
# (str) Custom source folders for requirements
|
||||
# Sets custom source for any requirements with recipes
|
||||
|
|
|
@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
|
|||
|
||||
# (list) Application requirements
|
||||
# comma seperated e.g. requirements = sqlite3,kivy
|
||||
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.36.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
|
||||
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.37.2#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git@a404269d91cff5358bcffb8067b0fd1d9c6842d3#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba@397ebe842856a4297bea5281206f9fa40a84699c#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru, hachoir
|
||||
|
||||
# (str) Custom source folders for requirements
|
||||
# Sets custom source for any requirements with recipes
|
||||
|
|
|
@ -2,5 +2,6 @@
|
|||
<style name="LbryAppTheme" parent="@android:style/Theme.Material.Light">
|
||||
<item name="android:windowBackground">@color/lbrygreen</item>
|
||||
<item name="colorControlActivated">@color/white</item>
|
||||
<item name="colorAccent">@color/white</item>
|
||||
</style>
|
||||
</resources>
|
357
src/main/java/io/lbry/browser/DownloadManager.java
Normal file
|
@ -0,0 +1,357 @@
|
|||
package io.lbry.browser;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
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.browser.receivers.NotificationDeletedReceiver;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Random;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class DownloadManager {
|
||||
private Context context;
|
||||
|
||||
private List<String> activeDownloads = new ArrayList<String>();
|
||||
|
||||
private List<String> completedDownloads = new ArrayList<String>();
|
||||
|
||||
private HashMap<Integer, NotificationCompat.Builder> builders = new HashMap<Integer, NotificationCompat.Builder>();
|
||||
|
||||
private HashMap<String, Integer> downloadIdNotificationIdMap = new HashMap<String, Integer>();
|
||||
|
||||
private HashMap<String, Boolean> stoppedDownloadsMap = new HashMap<String, Boolean>();
|
||||
|
||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
|
||||
|
||||
private static final int MAX_FILENAME_LENGTH = 20;
|
||||
|
||||
private static final int MAX_PROGRESS = 100;
|
||||
|
||||
private static final String GROUP_DOWNLOADS = "io.lbry.browser.GROUP_DOWNLOADS";
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DOWNLOADS_NOTIFICATION_CHANNEL";
|
||||
|
||||
private static boolean channelCreated = false;
|
||||
|
||||
private static NotificationCompat.Builder groupBuilder = null;
|
||||
|
||||
public static final String NOTIFICATION_ID_KEY = "io.lbry.browser.notificationId";
|
||||
|
||||
public static final String ACTION_DOWNLOAD_EVENT = "io.lbry.browser.ACTION_DOWNLOAD_EVENT";
|
||||
|
||||
public static final String ACTION_START = "start";
|
||||
|
||||
public static final String ACTION_COMPLETE = "complete";
|
||||
|
||||
public static final String ACTION_UPDATE = "update";
|
||||
|
||||
public static final int DOWNLOAD_NOTIFICATION_GROUP_ID = 20;
|
||||
|
||||
public static boolean groupCreated = false;
|
||||
|
||||
public DownloadManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
private int generateNotificationId() {
|
||||
int id = 0;
|
||||
Random random = new Random();
|
||||
do {
|
||||
id = random.nextInt();
|
||||
} while (id < 1000);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
// Only applies to Android 8.0 Oreo (API Level 26) or higher
|
||||
if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID, "LBRY Downloads", NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription("LBRY file downloads");
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotificationGroup() {
|
||||
if (!groupCreated) {
|
||||
Intent intent = new Intent(context, NotificationDeletedReceiver.class);
|
||||
intent.putExtra(NOTIFICATION_ID_KEY, DOWNLOAD_NOTIFICATION_GROUP_ID);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, DOWNLOAD_NOTIFICATION_GROUP_ID, intent, 0);
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
groupBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
groupBuilder.setContentTitle("Active LBRY downloads")
|
||||
// contentText will be displayed if there are no notifications in the group
|
||||
.setContentText("There are no active LBRY downloads.")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setGroupSummary(true)
|
||||
.setDeleteIntent(pendingIntent);
|
||||
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
|
||||
|
||||
groupCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static PendingIntent getLaunchPendingIntent(String uri, Context context) {
|
||||
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
||||
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public void startDownload(String id, String filename) {
|
||||
if (filename == null || filename.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (!isDownloadActive(id)) {
|
||||
activeDownloads.add(id);
|
||||
}
|
||||
|
||||
createNotificationChannel();
|
||||
createNotificationGroup();
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
// The file URI is used as the unique ID
|
||||
builder.setContentIntent(getLaunchPendingIntent(id, context))
|
||||
.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setProgress(MAX_PROGRESS, 0, false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
|
||||
int notificationId = getNotificationId(id);
|
||||
downloadIdNotificationIdMap.put(id, notificationId);
|
||||
builders.put(notificationId, builder);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (groupCreated && groupBuilder != null) {
|
||||
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateDownload(String id, String filename, double writtenBytes, double totalBytes) {
|
||||
if (filename == null || filename.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
createNotificationChannel();
|
||||
createNotificationGroup();
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = null;
|
||||
int notificationId = getNotificationId(id);
|
||||
if (builders.containsKey(notificationId)) {
|
||||
I'm curious if this change was stylistic or necessary 😀 (I agree the new one is better) I'm curious if this change was stylistic or necessary :grinning: (I agree the new one is better)
I remember seeing a few cases where the value of I remember seeing a few cases where the value of `writtenBytes` was greater than `totalBytes`, so this ended up being necessary.
|
||||
builder = builders.get(notificationId);
|
||||
} else {
|
||||
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
builder.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
builders.put(notificationId, builder);
|
||||
}
|
||||
|
||||
double progress = (writtenBytes / totalBytes) * 100;
|
||||
builder.setContentIntent(getLaunchPendingIntent(id, context))
|
||||
.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setProgress(MAX_PROGRESS, new Double(progress).intValue(), false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (progress >= MAX_PROGRESS) {
|
||||
builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
|
||||
.setContentText(String.format("%s", formatBytes(totalBytes)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setProgress(0, 0, false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (downloadIdNotificationIdMap.containsKey(id)) {
|
||||
downloadIdNotificationIdMap.remove(id);
|
||||
}
|
||||
if (builders.containsKey(notificationId)) {
|
||||
builders.remove(notificationId);
|
||||
}
|
||||
|
||||
// If there are no more downloads and the group exists, set the icon to stop animating
|
||||
if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
|
||||
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
|
||||
}
|
||||
|
||||
completeDownload(id, filename, totalBytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void completeDownload(String id, String filename, double totalBytes) {
|
||||
synchronized (this) {
|
||||
if (isDownloadActive(id)) {
|
||||
activeDownloads.remove(id);
|
||||
}
|
||||
if (!isDownloadCompleted(id)) {
|
||||
completedDownloads.add(id);
|
||||
}
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = null;
|
||||
int notificationId = getNotificationId(id);
|
||||
if (builders.containsKey(notificationId)) {
|
||||
builder = builders.get(notificationId);
|
||||
} else {
|
||||
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
builder.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
builders.put(notificationId, builder);
|
||||
}
|
||||
|
||||
builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
|
||||
.setContentText(String.format("%s", formatBytes(totalBytes)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setProgress(0, 0, false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
// If there are no more downloads and the group exists, set the icon to stop animating
|
||||
checkGroupDownloadIcon(notificationManager);
|
||||
}
|
||||
}
|
||||
|
||||
public void abortDownload(String id) {
|
||||
synchronized (this) {
|
||||
if (downloadIdNotificationIdMap.containsKey(id)) {
|
||||
removeDownloadNotification(id);
|
||||
}
|
||||
activeDownloads.remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDownloadActive(String id) {
|
||||
return (activeDownloads.contains(id));
|
||||
}
|
||||
|
||||
public boolean isDownloadCompleted(String id) {
|
||||
return (completedDownloads.contains(id));
|
||||
}
|
||||
|
||||
public boolean hasActiveDownloads() {
|
||||
return activeDownloads.size() > 0;
|
||||
}
|
||||
|
||||
public List<String> getActiveDownloads() {
|
||||
return activeDownloads;
|
||||
}
|
||||
|
||||
public List<String> getCompletedDownloads() {
|
||||
return completedDownloads;
|
||||
}
|
||||
|
||||
public void deleteDownloadUri(String uri) {
|
||||
synchronized (this) {
|
||||
activeDownloads.remove(uri);
|
||||
completedDownloads.remove(uri);
|
||||
|
||||
if (downloadIdNotificationIdMap.containsKey(uri)) {
|
||||
removeDownloadNotification(uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void removeDownloadNotification(String id) {
|
||||
int notificationId = downloadIdNotificationIdMap.get(id);
|
||||
if (downloadIdNotificationIdMap.containsKey(id)) {
|
||||
downloadIdNotificationIdMap.remove(id);
|
||||
}
|
||||
if (builders.containsKey(notificationId)) {
|
||||
builders.remove(notificationId);
|
||||
}
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = builders.get(notificationId);
|
||||
notificationManager.cancel(notificationId);
|
||||
|
||||
checkGroupDownloadIcon(notificationManager);
|
||||
if (builders.values().size() == 0) {
|
||||
notificationManager.cancel(DOWNLOAD_NOTIFICATION_GROUP_ID);
|
||||
groupCreated = false;
|
||||
}
|
||||
}
|
||||
|
||||
private int getNotificationId(String id) {
|
||||
if (downloadIdNotificationIdMap.containsKey(id)) {
|
||||
return downloadIdNotificationIdMap.get(id);
|
||||
}
|
||||
|
||||
int notificationId = generateNotificationId();
|
||||
if (MainActivity.downloadNotificationIds != null &&
|
||||
!MainActivity.downloadNotificationIds.contains(notificationId)) {
|
||||
MainActivity.downloadNotificationIds.add(notificationId);
|
||||
}
|
||||
downloadIdNotificationIdMap.put(id, notificationId);
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
private void checkGroupDownloadIcon(NotificationManagerCompat notificationManager) {
|
||||
if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
|
||||
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
notificationManager.notify(DOWNLOAD_NOTIFICATION_GROUP_ID, groupBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private static 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)));
|
||||
}
|
||||
|
||||
private static String truncateFilename(String filename, int alternateMaxLength) {
|
||||
int maxLength = alternateMaxLength > 0 ? alternateMaxLength : MAX_FILENAME_LENGTH;
|
||||
if (filename.length() < maxLength) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Get the extension
|
||||
int dotIndex = filename.lastIndexOf(".");
|
||||
if (dotIndex > -1) {
|
||||
String extension = filename.substring(dotIndex);
|
||||
return String.format("%s...%s", filename.substring(0, maxLength - extension.length() - 4), extension);
|
||||
}
|
||||
|
||||
return String.format("%s...", filename.substring(0, maxLength - 3));
|
||||
}
|
||||
|
||||
private static String truncateFilename(String filename) {
|
||||
return truncateFilename(filename, 0);
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Binder;
|
||||
|
@ -17,12 +18,30 @@ import android.support.v4.app.NotificationCompat;
|
|||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.IOException;
|
||||
import java.io.DataOutputStream;
|
||||
import java.net.ConnectException;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.kivy.android.PythonService;
|
||||
import org.renpy.android.AssetExtract;
|
||||
import org.renpy.android.ResourceManager;
|
||||
|
@ -41,6 +60,12 @@ public class LbrynetService extends PythonService {
|
|||
|
||||
public static final String ACTION_STOP_SERVICE = "io.lbry.browser.ACTION_STOP_SERVICE";
|
||||
|
||||
public static final String ACTION_CHECK_DOWNLOADS = "io.lbry.browser.ACTION_CHECK_DOWNLOADS";
|
||||
|
||||
public static final String ACTION_QUEUE_DOWNLOAD = "io.lbry.browser.ACTION_QUEUE_DOWNLOAD";
|
||||
|
||||
public static final String ACTION_DELETE_DOWNLOAD = "io.lbry.browser.ACTION_DELETE_DOWNLOAD";
|
||||
|
||||
public static final String GROUP_SERVICE = "io.lbry.browser.GROUP_SERVICE";
|
||||
|
||||
public static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DAEMON_NOTIFICATION_CHANNEL";
|
||||
|
@ -49,8 +74,22 @@ public class LbrynetService extends PythonService {
|
|||
|
||||
public static LbrynetService serviceInstance;
|
||||
|
||||
private static final String SDK_URL = "http://127.0.0.1:5279";
|
||||
|
||||
private static final int SDK_POLL_INTERVAL = 500; // 500 milliseconds
|
||||
|
||||
private BroadcastReceiver stopServiceReceiver;
|
||||
|
||||
private BroadcastReceiver downloadReceiver;
|
||||
|
||||
private DownloadManager downloadManager;
|
||||
|
||||
private ScheduledExecutorService taskExecutor;
|
||||
|
||||
private ScheduledFuture taskExecutorHandle = null;
|
||||
|
||||
private boolean streamManagerReady = false;
|
||||
|
||||
@Override
|
||||
public boolean canDisplayNotification() {
|
||||
return true;
|
||||
|
@ -70,6 +109,29 @@ public class LbrynetService extends PythonService {
|
|||
}
|
||||
};
|
||||
registerReceiver(stopServiceReceiver, intentFilter);
|
||||
|
||||
IntentFilter downloadFilter = new IntentFilter();
|
||||
downloadFilter.addAction(ACTION_CHECK_DOWNLOADS);
|
||||
downloadFilter.addAction(ACTION_DELETE_DOWNLOAD);
|
||||
downloadFilter.addAction(ACTION_QUEUE_DOWNLOAD);
|
||||
downloadReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (ACTION_QUEUE_DOWNLOAD.equals(action)) {
|
||||
String outpoint = intent.getStringExtra("outpoint");
|
||||
if (outpoint != null && outpoint.trim().length() > 0) {
|
||||
LbrynetService.this.queueDownload(outpoint);
|
||||
}
|
||||
} else if (ACTION_DELETE_DOWNLOAD.equals(action)) {
|
||||
String uri = intent.getStringExtra("uri");
|
||||
LbrynetService.this.deleteDownload(uri);
|
||||
} else if (ACTION_CHECK_DOWNLOADS.equals(action)) {
|
||||
LbrynetService.this.checkDownloads();
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(downloadReceiver, downloadFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -78,6 +140,7 @@ public class LbrynetService extends PythonService {
|
|||
String serviceDescription = "The LBRY service is running in the background.";
|
||||
|
||||
Context context = getApplicationContext();
|
||||
downloadManager = new DownloadManager(context);
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
|
@ -118,6 +181,286 @@ public class LbrynetService extends PythonService {
|
|||
startForeground(1, notification);
|
||||
}
|
||||
|
||||
private void checkDownloads() {
|
||||
if (taskExecutor == null) {
|
||||
taskExecutor = Executors.newScheduledThreadPool(1);
|
||||
taskExecutorHandle = taskExecutor.scheduleAtFixedRate(new Runnable() {
|
||||
public void run() {
|
||||
LbrynetService.this.pollFileList();
|
||||
}
|
||||
}, 0, SDK_POLL_INTERVAL, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
}
|
||||
|
||||
private String sdkCall(String method) throws ConnectException {
|
||||
return sdkCall(method, null);
|
||||
}
|
||||
|
||||
private String sdkCall(String method, Map<String, String> params) throws ConnectException {
|
||||
BufferedReader reader = null;
|
||||
DataOutputStream dos = null;
|
||||
HttpURLConnection conn = null;
|
||||
|
||||
try {
|
||||
JSONObject request = new JSONObject();
|
||||
request.put("method", method);
|
||||
if (params != null) {
|
||||
JSONObject requestParams = new JSONObject();
|
||||
for (Map.Entry<String, String> entry : params.entrySet()) {
|
||||
requestParams.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
request.put("params", requestParams);
|
||||
}
|
||||
|
||||
URL url = new URL(SDK_URL);
|
||||
conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setDoInput(true);
|
||||
conn.setDoOutput(true);
|
||||
conn.setUseCaches(false);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-type", "application/json");
|
||||
|
||||
dos = new DataOutputStream(conn.getOutputStream());
|
||||
dos.writeBytes(request.toString());
|
||||
dos.flush();
|
||||
dos.close();
|
||||
|
||||
if (conn.getResponseCode() == 200) {
|
||||
reader = new BufferedReader(new InputStreamReader(conn.getInputStream(), "utf-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String input;
|
||||
while ((input = reader.readLine()) != null) {
|
||||
sb.append(input);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
} else {
|
||||
reader = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "utf-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
String error;
|
||||
while ((error = reader.readLine()) != null) {
|
||||
sb.append(error);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
} catch (ConnectException ex) {
|
||||
// sdk not started yet. rethrow
|
||||
throw ex;
|
||||
} catch (IOException ex) {
|
||||
Log.e(TAG, ex.getMessage(), ex);
|
||||
// ignore and continue
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG, ex.getMessage(), ex);
|
||||
// ignore
|
||||
} finally {
|
||||
try {
|
||||
if (reader != null) {
|
||||
reader.close();
|
||||
}
|
||||
if (conn != null) {
|
||||
conn.disconnect();
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void pollFileList() {
|
||||
try {
|
||||
if (!streamManagerReady) {
|
||||
String statusResponse = sdkCall("status");
|
||||
if (statusResponse != null) {
|
||||
JSONObject status = new JSONObject(statusResponse);
|
||||
if (status.has("error")) {
|
||||
return;
|
||||
}
|
||||
if (status.has("result")) {
|
||||
JSONObject result = status.getJSONObject("result");
|
||||
if (result.has("startup_status")) {
|
||||
JSONObject startupStatus = result.getJSONObject("startup_status");
|
||||
streamManagerReady = startupStatus.has("stream_manager") && startupStatus.getBoolean("stream_manager");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (streamManagerReady) {
|
||||
String fileList = sdkCall("file_list");
|
||||
if (fileList != null) {
|
||||
JSONObject response = new JSONObject(fileList);
|
||||
if (!response.has("error")) {
|
||||
handlePollFileResponse(response);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ConnectException ex) {
|
||||
// pass
|
||||
} catch (JSONException ex) {
|
||||
Log.e(TAG, ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void queueDownload(String outpoint) {
|
||||
(new AsyncTask<Void, Void, String>() {
|
||||
protected String doInBackground(Void... param) {
|
||||
try {
|
||||
Map<String, String> params = new HashMap<String, String>();
|
||||
params.put("outpoint", outpoint);
|
||||
return sdkCall("file_list", params);
|
||||
} catch (ConnectException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected void onPostExecute(String fileList) {
|
||||
if (fileList != null) {
|
||||
try {
|
||||
JSONObject response = new JSONObject(fileList);
|
||||
if (!response.has("error")) {
|
||||
JSONArray fileItems = response.optJSONArray("result");
|
||||
if (fileItems != null && fileItems.length() > 0) {
|
||||
// TODO: Create Java FileItem class
|
||||
JSONObject item = fileItems.getJSONObject(0);
|
||||
String downloadPath = item.isNull("download_path") ? null : item.getString("download_path");
|
||||
if (downloadPath == null || downloadPath.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
String claimId = item.getString("claim_id");
|
||||
String claimName = item.getString("claim_name");
|
||||
String uri = String.format("lbry://%s#%s", claimName, claimId);
|
||||
|
||||
if (!downloadManager.isDownloadActive(uri) && !downloadManager.isDownloadCompleted(uri)) {
|
||||
File file = new File(downloadPath);
|
||||
Intent intent = createDownloadEventIntent(uri, outpoint, item.toString());
|
||||
intent.putExtra("action", "start");
|
||||
downloadManager.startDownload(uri, file.getName());
|
||||
|
||||
Context context = getApplicationContext();
|
||||
if (context != null) {
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
|
||||
checkDownloads();
|
||||
}
|
||||
}).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
}
|
||||
|
||||
private void deleteDownload(String uri) {
|
||||
if (downloadManager.isDownloadActive(uri)) {
|
||||
downloadManager.abortDownload(uri);
|
||||
}
|
||||
downloadManager.deleteDownloadUri(uri);
|
||||
}
|
||||
|
||||
private void handlePollFileResponse(JSONObject response) {
|
||||
Context context = getApplicationContext();
|
||||
if (response.has("result")) {
|
||||
JSONArray fileItems = response.optJSONArray("result");
|
||||
if (fileItems != null) {
|
||||
try {
|
||||
//List<String> itemUris = new ArrayList<String>();
|
||||
for (int i = 0; i < fileItems.length(); i++) {
|
||||
JSONObject item = fileItems.getJSONObject(i);
|
||||
String downloadPath = item.isNull("download_path") ? null : item.getString("download_path");
|
||||
if (downloadPath == null || downloadPath.trim().length() == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String claimId = item.getString("claim_id");
|
||||
String claimName = item.getString("claim_name");
|
||||
String uri = String.format("lbry://%s#%s", claimName, claimId);
|
||||
boolean completed = item.getBoolean("completed");
|
||||
double writtenBytes = item.optDouble("written_bytes", -1);
|
||||
double totalBytes = item.optDouble("total_bytes", -1);
|
||||
String outpoint = item.getString("outpoint");
|
||||
|
||||
if (downloadManager.isDownloadActive(uri) && (writtenBytes == -1 || totalBytes == -1)) {
|
||||
// possibly deleted, abort the download
|
||||
downloadManager.abortDownload(uri);
|
||||
continue;
|
||||
}
|
||||
|
||||
File file = new File(downloadPath);
|
||||
Intent intent = createDownloadEventIntent(uri, outpoint, item.toString());
|
||||
if (downloadManager.isDownloadActive(uri)) {
|
||||
if (writtenBytes >= totalBytes || completed) {
|
||||
// completed download
|
||||
intent.putExtra("action", "complete");
|
||||
downloadManager.completeDownload(uri, file.getName(), totalBytes);
|
||||
} else {
|
||||
intent.putExtra("action", "update");
|
||||
intent.putExtra("progress", (writtenBytes / totalBytes) * 100);
|
||||
downloadManager.updateDownload(uri, file.getName(), writtenBytes, totalBytes);
|
||||
}
|
||||
|
||||
if (context != null) {
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
} else {
|
||||
if (writtenBytes == -1 || writtenBytes >= totalBytes) {
|
||||
// do not start a download that is considered completed
|
||||
continue;
|
||||
}
|
||||
if (!completed && downloadPath != null) {
|
||||
intent.putExtra("action", "start");
|
||||
downloadManager.startDownload(uri, file.getName());
|
||||
if (context != null) {
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check download manager uris and clear downloads that may have been cancelled / deleted
|
||||
/*List<String> activeUris = downloadManager.getActiveDownloads();
|
||||
for (int i = 0; i < activeUris.size(); i++) {
|
||||
String activeUri = activeUris.get(i);
|
||||
if (!itemUris.contains(activeUri)) {
|
||||
downloadManager.abortDownload(activeUri);
|
||||
fileListUris.remove(activeUri); // remove URIs from the session that may have been deleted
|
||||
}
|
||||
}*/
|
||||
} catch (JSONException ex) {
|
||||
// pass
|
||||
Log.e(TAG, ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!downloadManager.hasActiveDownloads()) {
|
||||
// stop polling
|
||||
if (taskExecutorHandle != null) {
|
||||
taskExecutorHandle.cancel(true);
|
||||
taskExecutorHandle = null;
|
||||
}
|
||||
if (taskExecutor != null) {
|
||||
taskExecutor.shutdownNow();
|
||||
taskExecutor = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static Intent createDownloadEventIntent(String uri, String outpoint, String fileInfo) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
|
||||
intent.putExtra("uri", uri);
|
||||
intent.putExtra("outpoint", outpoint);
|
||||
intent.putExtra("file_info", fileInfo);
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
Looks like you could use the reverse of https://github.com/havardh/javaflow Looks like you could use the reverse of https://github.com/havardh/javaflow
I continue to think it would be really cool if there was a way for a single definition of I continue to think it would be really cool if there was a way for a single definition of `Claim`, `File`, etc. to be propagated from the SDK to a variety of languages. We ourselves would presumably use it in JavaScript, Java, Go. (@lyoshenka @eukreign no need to comment, but pointing out a use-case)
|
||||
public int startType() {
|
||||
return START_STICKY;
|
||||
|
@ -137,13 +480,19 @@ public class LbrynetService extends PythonService {
|
|||
getApplicationContext(), "", LbrynetService.class, "lbrynetservice");
|
||||
}
|
||||
|
||||
// Register broadcast receiver
|
||||
// no need to iterate the checks repeatedly here, because this is service startup
|
||||
checkDownloads();
|
||||
|
||||
return super.onStartCommand(intent, flags, startId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
if (downloadReceiver != null) {
|
||||
unregisterReceiver(downloadReceiver);
|
||||
downloadReceiver = null;
|
||||
}
|
||||
|
||||
if (stopServiceReceiver != null) {
|
||||
unregisterReceiver(stopServiceReceiver);
|
||||
stopServiceReceiver = null;
|
||||
|
|
|
@ -28,6 +28,7 @@ import com.facebook.react.ReactRootView;
|
|||
import com.facebook.react.ReactInstanceManager;
|
||||
import com.facebook.react.bridge.Arguments;
|
||||
import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.WritableArray;
|
||||
import com.facebook.react.bridge.WritableMap;
|
||||
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
||||
import com.facebook.react.shell.MainReactPackage;
|
||||
|
@ -39,16 +40,21 @@ import com.RNFetchBlob.RNFetchBlobPackage;
|
|||
|
||||
import io.lbry.browser.reactpackages.LbryReactPackage;
|
||||
import io.lbry.browser.reactmodules.BackgroundMediaModule;
|
||||
import io.lbry.browser.reactmodules.DownloadManagerModule;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
public class MainActivity extends Activity implements DefaultHardwareBackBtnHandler {
|
||||
|
||||
private static Activity currentActivity = null;
|
||||
|
@ -67,6 +73,8 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
|
||||
private BroadcastReceiver stopServiceReceiver;
|
||||
|
||||
private BroadcastReceiver downloadEventReceiver;
|
||||
|
||||
private ReactRootView mReactRootView;
|
||||
|
||||
private ReactInstanceManager mReactInstanceManager;
|
||||
|
@ -114,6 +122,9 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
// Register SMS receiver for handling verification texts
|
||||
registerSmsReceiver();
|
||||
|
||||
// Register the receiver to emit download events
|
||||
registerDownloadEventReceiver();
|
||||
|
||||
// Start the daemon service if it is not started
|
||||
serviceRunning = isServiceRunning(LbrynetService.class);
|
||||
if (!serviceRunning) {
|
||||
|
@ -142,6 +153,50 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
setContentView(mReactRootView);
|
||||
}
|
||||
|
||||
private void registerDownloadEventReceiver() {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_EVENT);
|
||||
downloadEventReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String downloadAction = intent.getStringExtra("action");
|
||||
String uri = intent.getStringExtra("uri");
|
||||
String outpoint = intent.getStringExtra("outpoint");
|
||||
String fileInfoJson = intent.getStringExtra("file_info");
|
||||
|
||||
if (uri == null || outpoint == null || fileInfoJson == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
String eventName = null;
|
||||
JSONObject json = new JSONObject(fileInfoJson);
|
||||
WritableMap fileInfo = JSONObjectToMap(json);
|
||||
WritableMap params = Arguments.createMap();
|
||||
params.putString("uri", uri);
|
||||
params.putString("outpoint", outpoint);
|
||||
params.putMap("fileInfo", fileInfo);
|
||||
|
||||
if (DownloadManager.ACTION_UPDATE.equals(downloadAction)) {
|
||||
double progress = intent.getDoubleExtra("progress", 0);
|
||||
params.putDouble("progress", progress);
|
||||
eventName = "onDownloadUpdated";
|
||||
} else {
|
||||
eventName = (DownloadManager.ACTION_START.equals(downloadAction)) ? "onDownloadStarted" : "onDownloadCompleted";
|
||||
}
|
||||
|
||||
ReactContext reactContext = mReactInstanceManager.getCurrentReactContext();
|
||||
if (reactContext != null) {
|
||||
reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params);
|
||||
}
|
||||
} catch (JSONException ex) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(downloadEventReceiver, intentFilter);
|
||||
}
|
||||
|
||||
private void registerStopReceiver() {
|
||||
IntentFilter intentFilter = new IntentFilter();
|
||||
intentFilter.addAction(LbrynetService.ACTION_STOP_SERVICE);
|
||||
|
@ -381,6 +436,11 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
smsReceiver = null;
|
||||
}
|
||||
|
||||
if (downloadEventReceiver != null) {
|
||||
unregisterReceiver(downloadEventReceiver);
|
||||
downloadEventReceiver = null;
|
||||
}
|
||||
|
||||
if (stopServiceReceiver != null) {
|
||||
unregisterReceiver(stopServiceReceiver);
|
||||
stopServiceReceiver = null;
|
||||
|
@ -388,7 +448,7 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
|
||||
notificationManager.cancel(BackgroundMediaModule.NOTIFICATION_ID);
|
||||
notificationManager.cancel(DownloadManagerModule.GROUP_ID);
|
||||
notificationManager.cancel(DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID);
|
||||
if (downloadNotificationIds != null) {
|
||||
for (int i = 0; i < downloadNotificationIds.size(); i++) {
|
||||
notificationManager.cancel(downloadNotificationIds.get(i));
|
||||
|
@ -478,4 +538,54 @@ public class MainActivity extends Activity implements DefaultHardwareBackBtnHand
|
|||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static WritableMap JSONObjectToMap(JSONObject jsonObject) throws JSONException {
|
||||
WritableMap map = Arguments.createMap();
|
||||
Iterator<String> keys = jsonObject.keys();
|
||||
while(keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
Object value = jsonObject.get(key);
|
||||
if (value instanceof JSONArray) {
|
||||
map.putArray(key, JSONArrayToList((JSONArray) value));
|
||||
} else if (value instanceof JSONObject) {
|
||||
map.putMap(key, JSONObjectToMap((JSONObject) value));
|
||||
} else if (value instanceof Boolean) {
|
||||
map.putBoolean(key, (Boolean) value);
|
||||
} else if (value instanceof Integer) {
|
||||
map.putInt(key, (Integer) value);
|
||||
} else if (value instanceof Double) {
|
||||
map.putDouble(key, (Double) value);
|
||||
} else if (value instanceof String) {
|
||||
map.putString(key, (String) value);
|
||||
} else {
|
||||
map.putString(key, value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
private static WritableArray JSONArrayToList(JSONArray jsonArray) throws JSONException {
|
||||
WritableArray array = Arguments.createArray();
|
||||
for(int i = 0; i < jsonArray.length(); i++) {
|
||||
Object value = jsonArray.get(i);
|
||||
if (value instanceof JSONArray) {
|
||||
array.pushArray(JSONArrayToList((JSONArray) value));
|
||||
} else if (value instanceof JSONObject) {
|
||||
array.pushMap(JSONObjectToMap((JSONObject) value));
|
||||
} else if (value instanceof Boolean) {
|
||||
array.pushBoolean((Boolean) value);
|
||||
} else if (value instanceof Integer) {
|
||||
array.pushInt((Integer) value);
|
||||
} else if (value instanceof Double) {
|
||||
array.pushDouble((Double) value);
|
||||
} else if (value instanceof String) {
|
||||
array.pushString((String) value);
|
||||
} else {
|
||||
array.pushString(value.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,299 +0,0 @@
|
|||
package io.lbry.browser.reactmodules;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
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.browser.MainActivity;
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.receivers.NotificationDeletedReceiver;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Random;
|
||||
|
||||
public class DownloadManagerModule 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 HashMap<String, Boolean> stoppedDownloadsMap = new HashMap<String, Boolean>();
|
||||
|
||||
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#");
|
||||
|
||||
private static final int MAX_FILENAME_LENGTH = 20;
|
||||
|
||||
private static final int MAX_PROGRESS = 100;
|
||||
|
||||
private static final String GROUP_DOWNLOADS = "io.lbry.browser.GROUP_DOWNLOADS";
|
||||
|
||||
private static final String NOTIFICATION_CHANNEL_ID = "io.lbry.browser.DOWNLOADS_NOTIFICATION_CHANNEL";
|
||||
|
||||
private static boolean channelCreated = false;
|
||||
|
||||
public static final String NOTIFICATION_ID_KEY = "io.lbry.browser.notificationId";
|
||||
|
||||
public static final int GROUP_ID = 20;
|
||||
|
||||
private static NotificationCompat.Builder groupBuilder = null;
|
||||
|
||||
public static boolean groupCreated = false;
|
||||
|
||||
public DownloadManagerModule(ReactApplicationContext reactContext) {
|
||||
super(reactContext);
|
||||
this.context = reactContext;
|
||||
}
|
||||
|
||||
private int generateNotificationId() {
|
||||
int id = 0;
|
||||
Random random = new Random();
|
||||
do {
|
||||
id = random.nextInt();
|
||||
} while (id < 1000);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "LbryDownloadManager";
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
// Only applies to Android 8.0 Oreo (API Level 26) or higher
|
||||
if (!channelCreated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID, "LBRY Downloads", NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription("LBRY file downloads");
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotificationGroup() {
|
||||
if (!groupCreated) {
|
||||
Intent intent = new Intent(context, NotificationDeletedReceiver.class);
|
||||
intent.putExtra(NOTIFICATION_ID_KEY, GROUP_ID);
|
||||
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, GROUP_ID, intent, 0);
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
groupBuilder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
groupBuilder.setContentTitle("Active LBRY downloads")
|
||||
// contentText will be displayed if there are no notifications in the group
|
||||
.setContentText("There are no active LBRY downloads.")
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setGroupSummary(true)
|
||||
.setDeleteIntent(pendingIntent);
|
||||
notificationManager.notify(GROUP_ID, groupBuilder.build());
|
||||
|
||||
groupCreated = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static PendingIntent getLaunchPendingIntent(String uri, Context context) {
|
||||
Intent launchIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
|
||||
launchIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
PendingIntent intent = PendingIntent.getActivity(context, 0, launchIntent, 0);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void startDownload(String id, String filename) {
|
||||
if (filename == null || filename.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
createNotificationChannel();
|
||||
createNotificationGroup();
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
// The file URI is used as the unique ID
|
||||
builder.setContentIntent(getLaunchPendingIntent(id, context))
|
||||
.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setProgress(MAX_PROGRESS, 0, false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
|
||||
int notificationId = getNotificationId(id);
|
||||
downloadIdNotificationIdMap.put(id, notificationId);
|
||||
builders.put(notificationId, builder);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (groupCreated && groupBuilder != null) {
|
||||
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
notificationManager.notify(GROUP_ID, groupBuilder.build());
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void updateDownload(String id, String filename, double progress, double writtenBytes, double totalBytes) {
|
||||
if (filename == null || filename.trim().length() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int notificationId = getNotificationId(id);
|
||||
if (notificationId == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (stoppedDownloadsMap.containsKey(id) && stoppedDownloadsMap.get(id)) {
|
||||
// if this happens, the download was canceled, so remove the notification
|
||||
// TODO: Figure out why updateDownload is called in the React Native code after stopDownload
|
||||
removeDownloadNotification(id);
|
||||
stoppedDownloadsMap.remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
createNotificationChannel();
|
||||
createNotificationGroup();
|
||||
|
||||
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
|
||||
|
||||
NotificationCompat.Builder builder = null;
|
||||
if (builders.containsKey(notificationId)) {
|
||||
builder = builders.get(notificationId);
|
||||
} else {
|
||||
builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
builder.setContentTitle(String.format("Downloading %s", truncateFilename(filename)))
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW);
|
||||
builders.put(notificationId, builder);
|
||||
}
|
||||
|
||||
builder.setContentIntent(getLaunchPendingIntent(id, context))
|
||||
.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setProgress(MAX_PROGRESS, new Double(progress).intValue(), false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (progress == MAX_PROGRESS) {
|
||||
builder.setContentTitle(String.format("Downloaded %s", truncateFilename(filename, 30)))
|
||||
.setContentText(String.format("%s", formatBytes(totalBytes)))
|
||||
.setGroup(GROUP_DOWNLOADS)
|
||||
.setProgress(0, 0, false)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
notificationManager.notify(notificationId, builder.build());
|
||||
|
||||
if (downloadIdNotificationIdMap.containsKey(id)) {
|
||||
downloadIdNotificationIdMap.remove(id);
|
||||
}
|
||||
if (builders.containsKey(notificationId)) {
|
||||
builders.remove(notificationId);
|
||||
}
|
||||
|
||||
// If there are no more downloads and the group exists, set the icon to stop animating
|
||||
if (groupCreated && groupBuilder != null && downloadIdNotificationIdMap.size() == 0) {
|
||||
groupBuilder.setSmallIcon(android.R.drawable.stat_sys_download_done);
|
||||
notificationManager.notify(GROUP_ID, groupBuilder.build());
|
||||
}
|
||||
|
||||
String spKey = String.format("dl__%s", id);
|
||||
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = sp.edit();
|
||||
editor.remove(spKey);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void stopDownload(String id, String filename) {
|
||||
android.util.Log.d("ReactNativeJS", "Stop download for id=" + id + "; filename=" + filename);
|
||||
stoppedDownloadsMap.put(id, true);
|
||||
removeDownloadNotification(id);
|
||||
}
|
||||
|
||||
private void removeDownloadNotification(String id) {
|
||||
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);
|
||||
notificationManager.cancel(notificationId);
|
||||
|
||||
downloadIdNotificationIdMap.remove(id);
|
||||
builders.remove(notificationId);
|
||||
|
||||
if (builders.values().size() == 0) {
|
||||
notificationManager.cancel(GROUP_ID);
|
||||
groupCreated = false;
|
||||
}
|
||||
}
|
||||
|
||||
private int getNotificationId(String id) {
|
||||
String spKey = String.format("dl__%s", id);
|
||||
SharedPreferences sp = context.getSharedPreferences(MainActivity.SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
|
||||
int notificationId = sp.getInt(spKey, -1);
|
||||
if (notificationId == -1) {
|
||||
notificationId = generateNotificationId();
|
||||
SharedPreferences.Editor editor = sp.edit();
|
||||
editor.putInt(spKey, notificationId);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
if (MainActivity.downloadNotificationIds != null &&
|
||||
!MainActivity.downloadNotificationIds.contains(notificationId)) {
|
||||
MainActivity.downloadNotificationIds.add(notificationId);
|
||||
}
|
||||
downloadIdNotificationIdMap.put(id, notificationId);
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
private static 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)));
|
||||
}
|
||||
|
||||
private static String truncateFilename(String filename, int alternateMaxLength) {
|
||||
int maxLength = alternateMaxLength > 0 ? alternateMaxLength : MAX_FILENAME_LENGTH;
|
||||
if (filename.length() < maxLength) {
|
||||
return filename;
|
||||
}
|
||||
|
||||
// Get the extension
|
||||
int dotIndex = filename.lastIndexOf(".");
|
||||
if (dotIndex > -1) {
|
||||
String extension = filename.substring(dotIndex);
|
||||
return String.format("%s...%s", filename.substring(0, maxLength - extension.length() - 4), extension);
|
||||
}
|
||||
|
||||
return String.format("%s...", filename.substring(0, maxLength - 3));
|
||||
}
|
||||
|
||||
private static String truncateFilename(String filename) {
|
||||
return truncateFilename(filename, 0);
|
||||
}
|
||||
}
|
|
@ -53,21 +53,12 @@ public class FirebaseModule extends ReactContextBaseJavaModule {
|
|||
}
|
||||
|
||||
@ReactMethod
|
||||
public void logException(boolean fatal, String message, ReadableMap payload) {
|
||||
public void logException(boolean fatal, String message, String error) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString("message", message);
|
||||
if (payload != null) {
|
||||
HashMap<String, Object> payloadMap = payload.toHashMap();
|
||||
for (Map.Entry<String, Object> entry : payloadMap.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value != null) {
|
||||
bundle.putString(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bundle.putString("error", error);
|
||||
if (firebaseAnalytics != null) {
|
||||
firebaseAnalytics.logEvent(fatal ? "exception" : "warning", bundle);
|
||||
firebaseAnalytics.logEvent(fatal ? "reactjs_exception" : "reactjs_warning", bundle);
|
||||
}
|
||||
|
||||
if (fatal) {
|
||||
|
|
|
@ -35,10 +35,11 @@ import java.util.Map;
|
|||
import java.util.Random;
|
||||
import java.security.KeyStore;
|
||||
|
||||
import io.lbry.browser.DownloadManager;
|
||||
import io.lbry.browser.MainActivity;
|
||||
import io.lbry.browser.LbrynetService;
|
||||
import io.lbry.browser.R;
|
||||
import io.lbry.browser.Utils;
|
||||
import io.lbry.browser.reactmodules.DownloadManagerModule;
|
||||
|
||||
public class UtilityModule extends ReactContextBaseJavaModule {
|
||||
private static final Map<String, Integer> activeNotifications = new HashMap<String, Integer>();
|
||||
|
@ -194,14 +195,13 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
if (fileUri != null) {
|
||||
Intent shareIntent = new Intent();
|
||||
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
|
||||
// Android 6 and lower
|
||||
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
}
|
||||
shareIntent.setAction(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_STREAM, fileUri);
|
||||
context.startActivity(Intent.createChooser(shareIntent, "Send LBRY log"));
|
||||
|
||||
Intent sendLogIntent = Intent.createChooser(shareIntent, "Send LBRY log");
|
||||
sendLogIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
context.startActivity(sendLogIntent);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
errorCallback.invoke("The lbrynet.log file cannot be shared due to permission restrictions.");
|
||||
|
@ -240,7 +240,7 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID);
|
||||
builder.setAutoCancel(true)
|
||||
.setColor(ContextCompat.getColor(context, R.color.lbrygreen))
|
||||
.setContentIntent(DownloadManagerModule.getLaunchPendingIntent(uri, context))
|
||||
.setContentIntent(DownloadManager.getLaunchPendingIntent(uri, context))
|
||||
.setContentTitle(publisher)
|
||||
.setContentText(title)
|
||||
.setSmallIcon(R.drawable.ic_lbry)
|
||||
|
@ -325,4 +325,33 @@ public class UtilityModule extends ReactContextBaseJavaModule {
|
|||
|
||||
promise.resolve(Utils.getSecureValue(key, context, keyStore));
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void checkDownloads() {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(LbrynetService.ACTION_CHECK_DOWNLOADS);
|
||||
if (context != null) {
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void queueDownload(String outpoint) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(LbrynetService.ACTION_QUEUE_DOWNLOAD);
|
||||
intent.putExtra("outpoint", outpoint);
|
||||
if (context != null) {
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@ReactMethod
|
||||
public void deleteDownload(String uri) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(LbrynetService.ACTION_DELETE_DOWNLOAD);
|
||||
intent.putExtra("uri", uri);
|
||||
if (context != null) {
|
||||
context.sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import com.facebook.react.uimanager.ViewManager;
|
|||
|
||||
import io.lbry.browser.reactmodules.BackgroundMediaModule;
|
||||
import io.lbry.browser.reactmodules.DaemonServiceControlModule;
|
||||
import io.lbry.browser.reactmodules.DownloadManagerModule;
|
||||
import io.lbry.browser.reactmodules.FirstRunModule;
|
||||
import io.lbry.browser.reactmodules.FirebaseModule;
|
||||
import io.lbry.browser.reactmodules.ScreenOrientationModule;
|
||||
|
@ -30,7 +29,6 @@ public class LbryReactPackage implements ReactPackage {
|
|||
|
||||
modules.add(new BackgroundMediaModule(reactContext));
|
||||
modules.add(new DaemonServiceControlModule(reactContext));
|
||||
modules.add(new DownloadManagerModule(reactContext));
|
||||
modules.add(new FirstRunModule(reactContext));
|
||||
modules.add(new FirebaseModule(reactContext));
|
||||
modules.add(new ScreenOrientationModule(reactContext));
|
||||
|
|
|
@ -4,14 +4,14 @@ import android.content.BroadcastReceiver;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import io.lbry.browser.reactmodules.DownloadManagerModule;
|
||||
import io.lbry.browser.DownloadManager;
|
||||
|
||||
public class NotificationDeletedReceiver extends BroadcastReceiver {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int notificationId = intent.getExtras().getInt(DownloadManagerModule.NOTIFICATION_ID_KEY);
|
||||
if (DownloadManagerModule.GROUP_ID == notificationId) {
|
||||
DownloadManagerModule.groupCreated = false;
|
||||
int notificationId = intent.getExtras().getInt(DownloadManager.NOTIFICATION_ID_KEY);
|
||||
if (DownloadManager.DOWNLOAD_NOTIFICATION_GROUP_ID == notificationId) {
|
||||
DownloadManager.groupCreated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,7 +78,10 @@ def start():
|
|||
data_dir=f'{private_storage_dir}/lbrynet',
|
||||
wallet_dir=f'{private_storage_dir}/lbryum',
|
||||
download_dir=f'{lbrynet_android_utils.getInternalStorageDir(service.getApplicationContext())}/Download',
|
||||
blob_lru_cache_size=32,
|
||||
components_to_skip=[DHT_COMPONENT, HASH_ANNOUNCER_COMPONENT, PEER_PROTOCOL_SERVER_COMPONENT],
|
||||
save_blobs=False,
|
||||
save_files=False,
|
||||
use_upnp=False
|
||||
)
|
||||
|
||||
|
|
After Width: | Height: | Size: 134 B |
After Width: | Height: | Size: 100 B |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 134 B |
After Width: | Height: | Size: 167 B |
After Width: | Height: | Size: 207 B |
why 10? what is 10?
const positionSaveInterval = 10
is a better practice