implement my LBRY downloads page (#273)

This commit is contained in:
Akinwale Ariwodola 2018-08-31 08:56:41 +01:00 committed by GitHub
parent 002d8c7430
commit e9abbf256e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 395 additions and 133 deletions

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import AboutPage from '../page/about'; import AboutPage from '../page/about';
import DiscoverPage from '../page/discover'; import DiscoverPage from '../page/discover';
import DownloadsPage from '../page/downloads';
import FilePage from '../page/file'; import FilePage from '../page/file';
import FirstRunScreen from '../page/firstRun'; import FirstRunScreen from '../page/firstRun';
import RewardsPage from '../page/rewards'; import RewardsPage from '../page/rewards';
@ -40,8 +41,10 @@ import {
} from 'lbryinc'; } from 'lbryinc';
import { makeSelectClientSetting } from '../redux/selectors/settings'; import { makeSelectClientSetting } from '../redux/selectors/settings';
import { decode as atob } from 'base-64'; import { decode as atob } from 'base-64';
import NavigationButton from '../component/navigationButton'; import Colors from '../styles/colors';
import Constants from '../constants'; import Constants from '../constants';
import Icon from 'react-native-vector-icons/FontAwesome5';
import NavigationButton from '../component/navigationButton';
import discoverStyle from '../styles/discover'; import discoverStyle from '../styles/discover';
import searchStyle from '../styles/search'; import searchStyle from '../styles/search';
import SearchRightHeaderIcon from '../component/searchRightHeaderIcon'; 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({ const walletStack = StackNavigator({
Wallet: { Wallet: {
screen: WalletPage, screen: WalletPage,
@ -107,26 +130,36 @@ const walletStack = StackNavigator({
headerMode: 'screen' headerMode: 'screen'
}); });
const rewardsStack = StackNavigator({
Rewards: {
screen: RewardsPage,
navigationOptions: ({ navigation }) => ({
title: 'Rewards',
headerLeft: menuNavigationButton(navigation),
})
}
});
const drawer = DrawerNavigator({ const drawer = DrawerNavigator({
DiscoverStack: { screen: discoverStack }, DiscoverStack: { screen: discoverStack, navigationOptions: {
TrendingStack: { screen: trendingStack }, drawerIcon: ({ tintColor }) => <Icon name="compass" size={20} style={{ color: tintColor }} />
WalletStack: { screen: walletStack }, }},
Rewards: { screen: rewardsStack }, TrendingStack: { screen: trendingStack, navigationOptions: {
Settings: { screen: SettingsPage, navigationOptions: { drawerLockMode: 'locked-closed' } }, drawerIcon: ({ tintColor }) => <Icon name="fire" size={20} style={{ color: tintColor }} />
About: { screen: AboutPage, navigationOptions: { drawerLockMode: 'locked-closed' } } }},
MyLBRYStack: { screen: myLbryStack, navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="folder" size={20} style={{ color: tintColor }} />
}},
Rewards: { screen: rewardsStack, navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="trophy" size={20} style={{ color: tintColor }} />
}},
WalletStack: { screen: walletStack, navigationOptions: {
drawerIcon: ({ tintColor }) => <Icon name="wallet" size={20} style={{ color: tintColor }} />
}},
Settings: { screen: SettingsPage, navigationOptions: {
drawerLockMode: 'locked-closed',
drawerIcon: ({ tintColor }) => <Icon name="cog" size={20} style={{ color: tintColor }} />
}},
About: { screen: AboutPage, navigationOptions: {
drawerLockMode: 'locked-closed',
drawerIcon: ({ tintColor }) => <Icon name="info" size={20} style={{ color: tintColor }} />
}}
}, { }, {
drawerWidth: 300, drawerWidth: 300,
headerMode: 'none' headerMode: 'none',
contentOptions: {
activeTintColor: Colors.LbryGreen
}
}); });
export const AppNavigator = new StackNavigator({ export const AppNavigator = new StackNavigator({

View file

@ -7,10 +7,11 @@ import {
makeSelectIsUriResolving, makeSelectIsUriResolving,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectShowNsfw } from '../../redux/selectors/settings'; import { selectShowNsfw } from '../../redux/selectors/settings';
import SearchResultItem from './view'; import FileListItem from './view';
const select = (state, props) => ({ const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state), claim: makeSelectClaimForUri(props.uri)(state),
fileInfo: makeSelectFileInfoForUri(props.uri)(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state), isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state), metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state), isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
@ -21,4 +22,4 @@ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)) resolveUri: uri => dispatch(doResolveUri(uri))
}); });
export default connect(select, perform)(SearchResultItem); export default connect(select, perform)(FileListItem);

View file

@ -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 (
<View style={style}>
<TouchableOpacity style={style} onPress={onPress}>
<FileItemMedia style={fileListStyle.thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
resizeMode="cover"
title={title}
thumbnail={metadata ? metadata.thumbnail : null} />
<View style={fileListStyle.detailsContainer}>
{isResolving && (
<View>
<Text style={fileListStyle.uri}>{uri}</Text>
<View style={fileListStyle.row}>
<ActivityIndicator size={"small"} color={Colors.LbryGreen} />
</View>
</View>)}
{!isResolving && <Text style={fileListStyle.title}>{title || name}</Text>}
{!isResolving && channel &&
<Link style={fileListStyle.publisher} text={channel} onPress={() => {
const channelUri = normalizeURI(channel);
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
}} />}
{fileInfo &&
<View style={fileListStyle.downloadInfo}>
<Text style={fileListStyle.downloadStorage}>{this.getStorageForFileInfo(fileInfo)}</Text>
{!fileInfo.completed &&
<View style={fileListStyle.progress}>
<View style={[fileListStyle.progressCompleted, { flex: this.getDownloadProgress(fileInfo) } ]} />
<View style={[fileListStyle.progressRemaining, { flex: (100 - this.getDownloadProgress(fileInfo)) } ]} />
</View>}
</View>
}
</View>
</TouchableOpacity>
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
</View>
);
}
}
export default FileListItem;

View file

@ -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 (
<View style={style}>
<TouchableOpacity style={style} onPress={onPress}>
<FileItemMedia style={searchStyle.thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
resizeMode="cover"
title={title}
thumbnail={metadata ? metadata.thumbnail : null} />
<View style={searchStyle.detailsContainer}>
{isResolvingUri && (
<View>
<Text style={searchStyle.uri}>{uri}</Text>
<View style={searchStyle.row}>
<ActivityIndicator size={"small"} color={Colors.LbryGreen} />
</View>
</View>)}
{!isResolvingUri && <Text style={searchStyle.title}>{title || name}</Text>}
{!isResolvingUri && channel &&
<Link style={searchStyle.publisher} text={channel} onPress={() => {
const channelUri = normalizeURI(channel);
navigation.navigate({ routeName: 'File', key: channelUri, params: { uri: channelUri }});
}} />}
</View>
</TouchableOpacity>
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate({ routeName: 'Settings', key: 'settingsPage' })} />}
</View>
);
}
}
export default SearchResultItem;

View file

@ -45,12 +45,20 @@ class UriBar extends React.PureComponent {
} }
handleItemPress = (item) => { handleItemPress = (item) => {
const { navigation, updateSearchQuery } = this.props; const { navigation, onSearchSubmitted, updateSearchQuery } = this.props;
const { type, value } = item; const { type, value } = item;
Keyboard.dismiss(); Keyboard.dismiss();
if (SEARCH_TYPES.SEARCH === type) { if (SEARCH_TYPES.SEARCH === type) {
this.setState({ currentValue: value });
updateSearchQuery(value);
if (onSearchSubmitted) {
onSearchSubmitted(value);
return;
}
navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value }}); navigation.navigate({ routeName: 'Search', key: 'searchPage', params: { searchQuery: value }});
} else { } else {
const uri = normalizeURI(value); const uri = normalizeURI(value);

View file

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

View file

@ -0,0 +1,77 @@
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 (
<View style={downloadsStyle.container}>
{!fetching && !hasDownloads && <Text style={downloadsStyle.noDownloadsText}>You have not downloaded anything from LBRY yet.</Text>}
{fetching && !hasDownloads && <ActivityIndicator size="large" color={Colors.LbryGreen} style={downloadsStyle.loading} /> }
{hasDownloads &&
<FlatList
style={downloadsStyle.scrollContainer}
contentContainerStyle={downloadsStyle.scrollPadding}
renderItem={ ({item}) => (
<FileListItem
style={fileListStyle.item}
uri={this.uriFromFileInfo(item)}
navigation={navigation}
onPress={() => 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.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}
/>}
<FloatingWalletBalance navigation={navigation} />
<UriBar navigation={navigation} />
</View>
);
}
}
export default DownloadsPage;

View file

@ -64,6 +64,7 @@ class FilePage extends React.PureComponent {
const { isResolvingUri, resolveUri, navigation } = this.props; const { isResolvingUri, resolveUri, navigation } = this.props;
const { uri } = navigation.state.params; const { uri } = navigation.state.params;
if (!isResolvingUri) resolveUri(uri); if (!isResolvingUri) resolveUri(uri);
this.fetchFileInfo(this.props); this.fetchFileInfo(this.props);
@ -295,7 +296,7 @@ class FilePage extends React.PureComponent {
blackListedOutpoints, blackListedOutpoints,
navigation navigation
} = this.props; } = this.props;
const { uri } = navigation.state.params; const { uri, autoplay } = navigation.state.params;
let innerContent = null; let innerContent = null;
if ((isResolvingUri && !claim) || !claim) { if ((isResolvingUri && !claim) || !claim) {
@ -402,7 +403,7 @@ class FilePage extends React.PureComponent {
ref={(ref) => { this.player = ref; }} ref={(ref) => { this.player = ref; }}
uri={uri} uri={uri}
style={playerStyle} style={playerStyle}
autoPlay={this.state.autoPlayMedia} autoPlay={autoplay || this.state.autoPlayMedia}
onFullscreenToggled={this.handleFullscreenToggle} onFullscreenToggled={this.handleFullscreenToggle}
onLayout={(evt) => { onLayout={(evt) => {
if (!this.state.playerHeight) { if (!this.state.playerHeight) {

View file

@ -10,7 +10,7 @@ import {
} from 'react-native'; } from 'react-native';
import Colors from '../../styles/colors'; import Colors from '../../styles/colors';
import PageHeader from '../../component/pageHeader'; import PageHeader from '../../component/pageHeader';
import SearchResultItem from '../../component/searchResultItem'; import FileListItem from '../../component/fileListItem';
import FloatingWalletBalance from '../../component/floatingWalletBalance'; import FloatingWalletBalance from '../../component/floatingWalletBalance';
import UriBar from '../../component/uriBar'; import UriBar from '../../component/uriBar';
import searchStyle from '../../styles/search'; import searchStyle from '../../styles/search';
@ -38,7 +38,7 @@ class SearchPage extends React.PureComponent {
<Text style={searchStyle.noResultsText}>No results to display.</Text>} <Text style={searchStyle.noResultsText}>No results to display.</Text>}
<ScrollView style={searchStyle.scrollContainer} contentContainerStyle={searchStyle.scrollPadding}> <ScrollView style={searchStyle.scrollContainer} contentContainerStyle={searchStyle.scrollPadding}>
{!isSearching && uris && uris.length ? ( {!isSearching && uris && uris.length ? (
uris.map(uri => <SearchResultItem key={uri} uris.map(uri => <FileListItem key={uri}
uri={uri} uri={uri}
style={searchStyle.resultItem} style={searchStyle.resultItem}
navigation={navigation} navigation={navigation}

View file

@ -46,7 +46,7 @@ class SettingsPage extends React.PureComponent {
<View style={settingsStyle.row}> <View style={settingsStyle.row}>
<View style={settingsStyle.switchText}> <View style={settingsStyle.switchText}>
<Text style={settingsStyle.label}>Keep the daemon background service running when the app is suspended.</Text> <Text style={settingsStyle.label}>Keep the daemon background service running after closing the app</Text>
<Text style={settingsStyle.description}>Enable this option for quicker app launch and to keep the synchronisation with the blockchain up to date.</Text> <Text style={settingsStyle.description}>Enable this option for quicker app launch and to keep the synchronisation with the blockchain up to date.</Text>
</View> </View>
<View style={settingsStyle.switchContainer}> <View style={settingsStyle.switchContainer}>

View file

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

View file

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

View file

@ -1,11 +1,4 @@
import { Dimensions, StyleSheet } from 'react-native'; import { 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 searchStyle = StyleSheet.create({ const searchStyle = StyleSheet.create({
container: { container: {
@ -30,36 +23,12 @@ const searchStyle = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
marginTop: 16 marginTop: 16
}, },
thumbnail: {
width: thumbnailWidth,
height: thumbnailHeight,
marginRight: 16,
justifyContent: 'center'
},
detailsContainer: {
flex: 1
},
searchInput: { searchInput: {
width: '100%', width: '100%',
height: '100%', height: '100%',
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',
fontSize: 16 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: { noResultsText: {
textAlign: 'center', textAlign: 'center',
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',

View file

@ -25,12 +25,14 @@ const settingsStyle = StyleSheet.create({
}, },
label: { label: {
fontSize: 14, fontSize: 14,
fontFamily: 'Metropolis-Regular' fontFamily: 'Metropolis-Regular',
lineHeight: 18
}, },
description: { description: {
color: '#aaaaaa',
fontSize: 12, fontSize: 12,
fontFamily: 'Metropolis-Regular', fontFamily: 'Metropolis-Regular',
color: '#aaaaaa' lineHeight: 18
} }
}); });

View file

@ -157,7 +157,8 @@ public class DownloadManagerModule extends ReactContextBaseJavaModule {
builder.setContentIntent(getLaunchPendingIntent(id)) builder.setContentIntent(getLaunchPendingIntent(id))
.setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes))) .setContentText(String.format("%.0f%% (%s / %s)", progress, formatBytes(writtenBytes), formatBytes(totalBytes)))
.setGroup(GROUP_DOWNLOADS) .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()); notificationManager.notify(notificationId, builder.build());
if (progress == MAX_PROGRESS) { if (progress == MAX_PROGRESS) {