diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js index 7f6c350e..ba69c482 100644 --- a/app/src/component/AppNavigator.js +++ b/app/src/component/AppNavigator.js @@ -1,6 +1,7 @@ import React from 'react'; import AboutPage from '../page/about'; import DiscoverPage from '../page/discover'; +import DownloadsPage from '../page/downloads'; import FilePage from '../page/file'; import FirstRunScreen from '../page/firstRun'; import RewardsPage from '../page/rewards'; @@ -40,8 +41,10 @@ import { } from 'lbryinc'; import { makeSelectClientSetting } from '../redux/selectors/settings'; import { decode as atob } from 'base-64'; -import NavigationButton from '../component/navigationButton'; +import Colors from '../styles/colors'; import Constants from '../constants'; +import Icon from 'react-native-vector-icons/FontAwesome5'; +import NavigationButton from '../component/navigationButton'; import discoverStyle from '../styles/discover'; import searchStyle from '../styles/search'; import SearchRightHeaderIcon from '../component/searchRightHeaderIcon'; @@ -88,6 +91,26 @@ const trendingStack = StackNavigator({ } }); +const myLbryStack = StackNavigator({ + Downloads: { + screen: DownloadsPage, + navigationOptions: ({ navigation }) => ({ + title: 'My LBRY', + headerLeft: menuNavigationButton(navigation), + }) + } +}); + +const rewardsStack = StackNavigator({ + Rewards: { + screen: RewardsPage, + navigationOptions: ({ navigation }) => ({ + title: 'Rewards', + headerLeft: menuNavigationButton(navigation), + }) + } +}); + const walletStack = StackNavigator({ Wallet: { screen: WalletPage, @@ -107,26 +130,36 @@ const walletStack = StackNavigator({ headerMode: 'screen' }); -const rewardsStack = StackNavigator({ - Rewards: { - screen: RewardsPage, - navigationOptions: ({ navigation }) => ({ - title: 'Rewards', - headerLeft: menuNavigationButton(navigation), - }) - } -}); - const drawer = DrawerNavigator({ - DiscoverStack: { screen: discoverStack }, - TrendingStack: { screen: trendingStack }, - WalletStack: { screen: walletStack }, - Rewards: { screen: rewardsStack }, - Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } }, - About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } } + DiscoverStack: { screen: discoverStack, navigationOptions: { + drawerIcon: ({ tintColor }) => + }}, + TrendingStack: { screen: trendingStack, navigationOptions: { + drawerIcon: ({ tintColor }) => + }}, + MyLBRYStack: { screen: myLbryStack, navigationOptions: { + drawerIcon: ({ tintColor }) => + }}, + Rewards: { screen: rewardsStack, navigationOptions: { + drawerIcon: ({ tintColor }) => + }}, + WalletStack: { screen: walletStack, navigationOptions: { + drawerIcon: ({ tintColor }) => + }}, + Settings: { screen: SettingsPage, navigationOptions: { + drawerLockMode: 'locked-closed', + drawerIcon: ({ tintColor }) => + }}, + About: { screen: AboutPage, navigationOptions: { + drawerLockMode: 'locked-closed', + drawerIcon: ({ tintColor }) => + }} }, { drawerWidth: 300, - headerMode: 'none' + headerMode: 'none', + contentOptions: { + activeTintColor: Colors.LbryGreen + } }); export const AppNavigator = new StackNavigator({ diff --git a/app/src/component/searchResultItem/index.js b/app/src/component/fileListItem/index.js similarity index 81% rename from app/src/component/searchResultItem/index.js rename to app/src/component/fileListItem/index.js index e4b7d15d..f88b9118 100644 --- a/app/src/component/searchResultItem/index.js +++ b/app/src/component/fileListItem/index.js @@ -7,10 +7,11 @@ import { makeSelectIsUriResolving, } from 'lbry-redux'; import { selectShowNsfw } from '../../redux/selectors/settings'; -import SearchResultItem from './view'; +import FileListItem from './view'; const select = (state, props) => ({ claim: makeSelectClaimForUri(props.uri)(state), + fileInfo: makeSelectFileInfoForUri(props.uri)(state), isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state), @@ -21,4 +22,4 @@ const perform = dispatch => ({ resolveUri: uri => dispatch(doResolveUri(uri)) }); -export default connect(select, perform)(SearchResultItem); +export default connect(select, perform)(FileListItem); diff --git a/app/src/component/fileListItem/view.js b/app/src/component/fileListItem/view.js new file mode 100644 index 00000000..2d3c4ac4 --- /dev/null +++ b/app/src/component/fileListItem/view.js @@ -0,0 +1,113 @@ +import React from 'react'; +import { normalizeURI, parseURI } from 'lbry-redux'; +import { + ActivityIndicator, + Platform, + ProgressBarAndroid, + Text, + TouchableOpacity, + View +} from 'react-native'; +import Colors from '../../styles/colors'; +import FileItemMedia from '../fileItemMedia'; +import Link from '../../component/link'; +import NsfwOverlay from '../../component/nsfwOverlay'; +import fileListStyle from '../../styles/fileList'; + +class FileListItem extends React.PureComponent { + getStorageForFileInfo = (fileInfo) => { + if (!fileInfo.completed) { + const written = this.formatBytes(fileInfo.written_bytes); + const total = this.formatBytes(fileInfo.total_bytes); + return `(${written} / ${total})`; + } + + return this.formatBytes(fileInfo.written_bytes); + } + + formatBytes = (bytes) => { + if (bytes < 1048576) { // < 1MB + const value = (bytes / 1024.0).toFixed(2); + return `${value} KB`; + } + + if (bytes < 1073741824) { // < 1GB + const value = (bytes / (1024.0 * 1024.0)).toFixed(2); + return `${value} MB`; + } + + const value = (bytes / (1024.0 * 1024.0 * 1024.0)).toFixed(2); + return `${value} GB`; + } + + getDownloadProgress = (fileInfo) => { + return Math.ceil((fileInfo.written_bytes / fileInfo.total_bytes) * 100); + } + + render() { + const { + claim, + fileInfo, + metadata, + isResolvingUri, + isDownloaded, + style, + onPress, + navigation + } = this.props; + + const uri = normalizeURI(this.props.uri); + const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; + const isResolving = !fileInfo && isResolvingUri; + const title = fileInfo ? fileInfo.metadata.title : metadata && metadata.title ? metadata.title : parseURI(uri).contentName; + + let name; + let channel; + if (claim) { + name = claim.name; + channel = claim.channel_name; + } + + return ( + + + + + {isResolving && ( + + {uri} + + + + )} + + {!isResolving && {title || name}} + {!isResolving && channel && + { + const channelUri = normalizeURI(channel); + navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }}); + }} />} + + {fileInfo && + + {this.getStorageForFileInfo(fileInfo)} + {!fileInfo.completed && + + + + } + + } + + + {obscureNsfw && navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />} + + ); + } +} + +export default FileListItem; diff --git a/app/src/component/searchResultItem/view.js b/app/src/component/searchResultItem/view.js deleted file mode 100644 index 414e5286..00000000 --- a/app/src/component/searchResultItem/view.js +++ /dev/null @@ -1,64 +0,0 @@ -import React from 'react'; -import { normalizeURI, parseURI } from 'lbry-redux'; -import { ActivityIndicator, Text, TouchableOpacity, View } from 'react-native'; -import Colors from '../../styles/colors'; -import FileItemMedia from '../fileItemMedia'; -import Link from '../../component/link'; -import NsfwOverlay from '../../component/nsfwOverlay'; -import searchStyle from '../../styles/search'; - -class SearchResultItem extends React.PureComponent { - render() { - const { - claim, - metadata, - isResolvingUri, - showUri, - isDownloaded, - style, - onPress, - navigation - } = this.props; - - const uri = normalizeURI(this.props.uri); - const obscureNsfw = this.props.obscureNsfw && metadata && metadata.nsfw; - const title = metadata && metadata.title ? metadata.title : parseURI(uri).contentName; - - let name; - let channel; - if (claim) { - name = claim.name; - channel = claim.channel_name; - } - - return ( - - - - - {isResolvingUri && ( - - {uri} - - - - )} - {!isResolvingUri && {title || name}} - {!isResolvingUri && channel && - { - const channelUri = normalizeURI(channel); - navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }}); - }} />} - - - {obscureNsfw && navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />} - - ); - } -} - -export default SearchResultItem; diff --git a/app/src/page/downloads/index.js b/app/src/page/downloads/index.js new file mode 100644 index 00000000..11ffe215 --- /dev/null +++ b/app/src/page/downloads/index.js @@ -0,0 +1,20 @@ +import { connect } from 'react-redux'; +import { + doFileList, + selectFileInfosDownloaded, + selectMyClaimsWithoutChannels, + selectIsFetchingFileList, +} from 'lbry-redux'; +import DownloadsPage from './view'; + +const select = (state) => ({ + fileInfos: selectFileInfosDownloaded(state), + fetching: selectIsFetchingFileList(state), + claims: selectMyClaimsWithoutChannels(state), +}); + +const perform = dispatch => ({ + fileList: () => dispatch(doFileList()), +}); + +export default connect(select, perform)(DownloadsPage); diff --git a/app/src/page/downloads/view.js b/app/src/page/downloads/view.js new file mode 100644 index 00000000..eaede864 --- /dev/null +++ b/app/src/page/downloads/view.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { Lbry, buildURI } from 'lbry-redux'; +import { + ActivityIndicator, + Button, + FlatList, + Text, + TextInput, + View, + ScrollView +} from 'react-native'; +import Colors from '../../styles/colors'; +import PageHeader from '../../component/pageHeader'; +import FileListItem from '../../component/fileListItem'; +import FloatingWalletBalance from '../../component/floatingWalletBalance'; +import UriBar from '../../component/uriBar'; +import downloadsStyle from '../../styles/downloads'; +import fileListStyle from '../../styles/fileList'; + +class DownloadsPage extends React.PureComponent { + static navigationOptions = { + title: 'My LBRY' + }; + + componentDidMount() { + this.props.fileList(); + } + + uriFromFileInfo(fileInfo) { + const { name: claimName, claim_name: claimNameDownloaded, claim_id: claimId } = fileInfo; + const uriParams = {}; + uriParams.contentName = claimName || claimNameDownloaded; + uriParams.claimId = claimId; + return buildURI(uriParams); + } + + render() { + const { fetching, fileInfos, navigation } = this.props; + const hasDownloads = fileInfos && Object.values(fileInfos).length > 0; + + return ( + + {!fetching && !hasDownloads && You have not downloaded anything from LBRY yet.} + {fetching && !hasDownloads && } + {hasDownloads && + ( + navigation.navigate({ + routeName: 'File', + key: 'filePage', + params: { uri: this.uriFromFileInfo(item), autoplay: true } + })} /> + ) + } + data={fileInfos.sort((a, b) => { + // TODO: Implement sort based on user selection + if (!a.completed && b.completed) return -1; + if (a.metadata.title === b.metadata.title) return 0; + return (a.metadata.title < b.metadata.title) ? -1 : 1; + })} + keyExtractor={(item, index) => item.outpoint} + />} + + + + ); + } +} + +export default DownloadsPage; diff --git a/app/src/page/file/view.js b/app/src/page/file/view.js index d7bfb79c..8e96b5d5 100644 --- a/app/src/page/file/view.js +++ b/app/src/page/file/view.js @@ -64,6 +64,7 @@ class FilePage extends React.PureComponent { const { isResolvingUri, resolveUri, navigation } = this.props; const { uri } = navigation.state.params; + if (!isResolvingUri) resolveUri(uri); this.fetchFileInfo(this.props); @@ -295,7 +296,7 @@ class FilePage extends React.PureComponent { blackListedOutpoints, navigation } = this.props; - const { uri } = navigation.state.params; + const { uri, autoplay } = navigation.state.params; let innerContent = null; if ((isResolvingUri && !claim) || !claim) { @@ -402,7 +403,7 @@ class FilePage extends React.PureComponent { ref={(ref) => { this.player = ref; }} uri={uri} style={playerStyle} - autoPlay={this.state.autoPlayMedia} + autoPlay={autoplay || this.state.autoPlayMedia} onFullscreenToggled={this.handleFullscreenToggle} onLayout={(evt) => { if (!this.state.playerHeight) { diff --git a/app/src/page/search/view.js b/app/src/page/search/view.js index 8864db02..5a788cca 100644 --- a/app/src/page/search/view.js +++ b/app/src/page/search/view.js @@ -10,7 +10,7 @@ import { } from 'react-native'; import Colors from '../../styles/colors'; import PageHeader from '../../component/pageHeader'; -import SearchResultItem from '../../component/searchResultItem'; +import FileListItem from '../../component/fileListItem'; import FloatingWalletBalance from '../../component/floatingWalletBalance'; import UriBar from '../../component/uriBar'; import searchStyle from '../../styles/search'; @@ -38,15 +38,15 @@ class SearchPage extends React.PureComponent { No results to display.} {!isSearching && uris && uris.length ? ( - uris.map(uri => navigation.navigate({ - routeName: 'File', - key: 'filePage', - params: { uri }}) - }/>) + uris.map(uri => navigation.navigate({ + routeName: 'File', + key: 'filePage', + params: { uri }}) + }/>) ) : null } {isSearching && } diff --git a/app/src/styles/downloads.js b/app/src/styles/downloads.js new file mode 100644 index 00000000..ec04a9d1 --- /dev/null +++ b/app/src/styles/downloads.js @@ -0,0 +1,34 @@ +import { StyleSheet } from 'react-native'; + +const downloadsStyle = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center' + }, + itemList: { + flex: 1, + }, + scrollContainer: { + flex: 1, + width: '100%', + height: '100%', + paddingLeft: 16, + paddingRight: 16, + marginBottom: 60 + }, + scrollPadding: { + paddingBottom: 16 + }, + noDownloadsText: { + textAlign: 'center', + fontFamily: 'Metropolis-Regular', + fontSize: 14, + position: 'absolute' + }, + loading: { + position: 'absolute' + } +}); + +export default downloadsStyle; diff --git a/app/src/styles/fileList.js b/app/src/styles/fileList.js new file mode 100644 index 00000000..dfaec77a --- /dev/null +++ b/app/src/styles/fileList.js @@ -0,0 +1,67 @@ +import { Dimensions, StyleSheet } from 'react-native'; +import Colors from './colors'; + +const screenDimension = Dimensions.get('window'); +const screenWidth = screenDimension.width; +const screenHeight = screenDimension.height; +const thumbnailHeight = 100; +const thumbnailWidth = (screenHeight / screenWidth) * thumbnailHeight; + +const fileListStyle = StyleSheet.create({ + item: { + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + marginTop: 16 + }, + detailsContainer: { + flex: 1 + }, + thumbnail: { + width: thumbnailWidth, + height: thumbnailHeight, + marginRight: 16, + justifyContent: 'center' + }, + title: { + fontFamily: 'Metropolis-SemiBold', + fontSize: 16 + }, + uri: { + fontFamily: 'Metropolis-SemiBold', + fontSize: 14, + marginBottom: 8 + }, + publisher: { + fontFamily: 'Metropolis-SemiBold', + fontSize: 14, + marginTop: 3, + color: Colors.LbryGreen + }, + loading: { + position: 'absolute' + }, + downloadInfo: { + marginTop: 8 + }, + downloadStorage: { + fontFamily: 'Metropolis-Regular', + fontSize: 14, + color: Colors.ChannelGrey + }, + progress: { + marginTop: 4, + height: 3, + flex: 1, + flexDirection: 'row' + }, + progressCompleted: { + backgroundColor: Colors.LbryGreen + }, + progressRemaining: { + backgroundColor: Colors.LbryGreen, + opacity: 0.2 + } +}); + +export default fileListStyle; diff --git a/app/src/styles/search.js b/app/src/styles/search.js index f7357d3d..7d6c9711 100644 --- a/app/src/styles/search.js +++ b/app/src/styles/search.js @@ -1,11 +1,4 @@ -import { Dimensions, StyleSheet } from 'react-native'; -import Colors from './colors'; - -const screenDimension = Dimensions.get('window'); -const screenWidth = screenDimension.width; -const screenHeight = screenDimension.height; -const thumbnailHeight = 100; -const thumbnailWidth = (screenHeight / screenWidth) * thumbnailHeight; +import { StyleSheet } from 'react-native'; const searchStyle = StyleSheet.create({ container: { @@ -30,36 +23,12 @@ const searchStyle = StyleSheet.create({ justifyContent: 'space-between', marginTop: 16 }, - thumbnail: { - width: thumbnailWidth, - height: thumbnailHeight, - marginRight: 16, - justifyContent: 'center' - }, - detailsContainer: { - flex: 1 - }, searchInput: { width: '100%', height: '100%', fontFamily: 'Metropolis-Regular', fontSize: 16 }, - title: { - fontFamily: 'Metropolis-SemiBold', - fontSize: 16 - }, - uri: { - fontFamily: 'Metropolis-SemiBold', - fontSize: 14, - marginBottom: 8 - }, - publisher: { - fontFamily: 'Metropolis-SemiBold', - fontSize: 14, - marginTop: 3, - color: Colors.LbryGreen - }, noResultsText: { textAlign: 'center', fontFamily: 'Metropolis-Regular', diff --git a/src/main/java/io/lbry/browser/reactmodules/DownloadManagerModule.java b/src/main/java/io/lbry/browser/reactmodules/DownloadManagerModule.java index c493ec13..7795115b 100644 --- a/src/main/java/io/lbry/browser/reactmodules/DownloadManagerModule.java +++ b/src/main/java/io/lbry/browser/reactmodules/DownloadManagerModule.java @@ -157,7 +157,8 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule { builder.setContentIntent(getLaunchPendingIntent(id)) .setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes))) .setGroup(GROUP_DOWNLOADS) - .setProgress(MAX_PROGRESS, new Double(progress).intValue(), false); + .setProgress(MAX_PROGRESS, new Double(progress).intValue(), false) + .setSmallIcon(android.R.drawable.stat_sys_download); notificationManager.notify(notificationId, builder.build()); if (progress == MAX_PROGRESS) {