implement direct URI navigation (#134)

This commit is contained in:
akinwale 2018-05-24 23:47:55 +01:00 committed by GitHub
parent 077af85181
commit 4fbf90654e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 287 additions and 83 deletions

View file

@ -1,5 +1,6 @@
import { connect } from 'react-redux';
import {
doFetchCostInfoForUri,
makeSelectFileInfoForUri,
makeSelectDownloadingForUri,
makeSelectLoadingForUri,
@ -17,7 +18,8 @@ const select = (state, props) => ({
const perform = dispatch => ({
purchaseUri: uri => dispatch(doPurchaseUri(uri)),
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint))
restartDownload: (uri, outpoint) => dispatch(doStartDownload(uri, outpoint)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
});
export default connect(select, perform)(FileDownloadButton);

View file

@ -3,6 +3,13 @@ import { NativeModules, Text, View, TouchableOpacity } from 'react-native';
import fileDownloadButtonStyle from '../../styles/fileDownloadButton';
class FileDownloadButton extends React.PureComponent {
componentDidMount() {
const { costInfo, fetchCostInfo, uri } = this.props;
if (costInfo === undefined) {
fetchCostInfo(uri);
}
}
componentWillReceiveProps(nextProps) {
//this.checkAvailability(nextProps.uri);
this.restartDownload(nextProps);
@ -54,7 +61,7 @@ class FileDownloadButton extends React.PureComponent {
if (!costInfo) {
return (
<View style={[style, fileDownloadButtonStyle.container]}>
<Text>Fetching cost info...</Text>
<Text style={fileDownloadButtonStyle.text}>Fetching cost info...</Text>
</View>
);
}

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import UriBar from './view';
const select = state => ({
});
const perform = dispatch => ({
});
export default connect(select, perform)(UriBar);

View file

@ -0,0 +1,44 @@
// @flow
import React from 'react';
import { normalizeURI } from 'lbry-redux';
import { TextInput, View } from 'react-native';
import uriBarStyle from '../../styles/uriBar';
class UriBar extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
uri: null,
currentValue: null
};
}
render() {
const { value, navigation } = this.props;
if (!this.state.currentValue) {
this.setState({ currentValue: value });
}
// TODO: Search and URI suggestions overlay
return (
<View style={uriBarStyle.uriContainer}>
<TextInput style={uriBarStyle.uriText}
placeholder={'Enter a LBRY URI or some text'}
underlineColorAndroid={'transparent'}
numberOfLines={1}
value={this.state.currentValue}
returnKeyType={'go'}
onChangeText={(text) => this.setState({uri: text, currentValue: text})}
onSubmitEditing={() => {
if (this.state.uri) {
let uri = this.state.uri;
uri = uri.replace(/ /g, '-');
navigation.navigate('File', { uri: normalizeURI(uri) });
}
}}/>
</View>
);
}
}
export default UriBar;

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import {
makeSelectClaimForUri,
makeSelectClaimsInChannelForCurrentPage,
makeSelectFetchingChannelClaims,
} from 'lbry-redux';
import ChannelPage from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
claimsInChannel: makeSelectClaimsInChannelForCurrentPage(props.uri)(state),
fetching: makeSelectFetchingChannelClaims(props.uri)(state),
});
const perform = dispatch => ({
});
export default connect(select, perform)(ChannelPage);

View file

@ -0,0 +1,24 @@
// @flow
import React from 'react';
import { ScrollView, Text, View } from 'react-native';
import UriBar from '../../component/uriBar';
import channelPageStyle from '../../styles/channelPage';
class ChannelPage extends React.PureComponent {
render() {
const { claim, navigation, uri } = this.props;
const { name } = claim;
return (
<View style={channelPageStyle.container}>
<Text style={channelPageStyle.title}>{name}</Text>
<ScrollView style={channelPageStyle.content}>
</ScrollView>
<UriBar value={uri} navigation={navigation} />
</View>
)
}
}
export default ChannelPage;

View file

@ -1,10 +1,10 @@
import React from 'react';
import NavigationActions from 'react-navigation';
import {
ActivityIndicator,
AsyncStorage,
NativeModules,
SectionList,
ScrollView,
Text,
View
} from 'react-native';
@ -12,6 +12,8 @@ import { normalizeURI } from 'lbry-redux';
import moment from 'moment';
import FileItem from '../../component/fileItem';
import discoverStyle from '../../styles/discover';
import Colors from '../../styles/colors';
import UriBar from '../../component/uriBar';
import Feather from 'react-native-vector-icons/Feather';
class DiscoverPage extends React.PureComponent {
@ -48,7 +50,12 @@ class DiscoverPage extends React.PureComponent {
return (
<View style={discoverStyle.container}>
{!hasContent && fetchingFeaturedUris && <Text style={discoverStyle.title}>Fetching content...</Text>}
{!hasContent && fetchingFeaturedUris && (
<View style={discoverStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
<Text style={discoverStyle.title}>Fetching content...</Text>
</View>
)}
{hasContent &&
<SectionList style={discoverStyle.scrollContainer}
renderItem={ ({item, index, section}) => (
@ -66,6 +73,7 @@ class DiscoverPage extends React.PureComponent {
keyExtractor={(item, index) => item}
/>
}
<UriBar navigation={navigation} />
</View>
);
}

View file

@ -1,13 +1,16 @@
import { connect } from 'react-redux';
import {
doFetchFileInfo,
makeSelectFileInfoForUri,
doResolveUri,
doFetchCostInfoForUri,
makeSelectIsUriResolving,
makeSelectCostInfoForUri,
makeSelectFileInfoForUri,
makeSelectClaimForUri,
makeSelectContentTypeForUri,
makeSelectMetadataForUri,
selectRewardContentClaimIds,
makeSelectCostInfoForUri
selectBlackListedOutpoints,
} from 'lbry-redux';
import { doDeleteFile, doStopDownloadingFile } from '../../redux/actions/file';
import FilePage from './view';
@ -15,7 +18,9 @@ import FilePage from './view';
const select = (state, props) => {
const selectProps = { uri: props.navigation.state.params.uri };
return {
blackListedOutpoints: selectBlackListedOutpoints(state),
claim: makeSelectClaimForUri(selectProps.uri)(state),
isResolvingUri: makeSelectIsUriResolving(selectProps.uri)(state),
contentType: makeSelectContentTypeForUri(selectProps.uri)(state),
costInfo: makeSelectCostInfoForUri(selectProps.uri)(state),
metadata: makeSelectMetadataForUri(selectProps.uri)(state),
@ -29,6 +34,7 @@ const select = (state, props) => {
const perform = dispatch => ({
fetchFileInfo: uri => dispatch(doFetchFileInfo(uri)),
fetchCostInfo: uri => dispatch(doFetchCostInfoForUri(uri)),
resolveUri: uri => dispatch(doResolveUri(uri)),
stopDownload: (uri, fileInfo) => dispatch(doStopDownloadingFile(uri, fileInfo)),
deleteFile: (fileInfo, deleteFromDevice, abandonClaim) => {
dispatch(doDeleteFile(fileInfo, deleteFromDevice, abandonClaim));

View file

@ -13,9 +13,11 @@ import {
NativeModules
} from 'react-native';
import Colors from '../../styles/colors';
import ChannelPage from '../channel';
import FileItemMedia from '../../component/fileItemMedia';
import FileDownloadButton from '../../component/fileDownloadButton';
import MediaPlayer from '../../component/mediaPlayer';
import UriBar from '../../component/uriBar';
import Video from 'react-native-video';
import filePageStyle from '../../styles/filePage';
@ -34,15 +36,27 @@ class FilePage extends React.PureComponent {
componentDidMount() {
StatusBar.setHidden(false);
const { isResolvingUri, resolveUri, navigation } = this.props;
const { uri } = navigation.state.params;
if (!isResolvingUri) resolveUri(uri);
this.fetchFileInfo(this.props);
this.fetchCostInfo(this.props);
if (NativeModules.Mixpanel) {
NativeModules.Mixpanel.track('Open File Page', { Uri: this.props.navigation.state.params.uri });
NativeModules.Mixpanel.track('Open File Page', { Uri: uri });
}
}
componentWillReceiveProps(nextProps) {
this.fetchFileInfo(nextProps);
componentDidUpdate() {
this.fetchFileInfo(this.props);
const { isResolvingUri, resolveUri, claim, navigation } = this.props;
const { uri } = navigation.state.params;
if (!isResolvingUri && claim === undefined && uri) {
resolveUri(uri);
}
}
fetchFileInfo(props) {
@ -113,72 +127,87 @@ class FilePage extends React.PureComponent {
contentType,
tab,
rewardedContentClaimIds,
isResolvingUri,
blackListedOutpoints,
navigation
} = this.props;
} = this.props;
const { uri } = navigation.state.params;
if (!claim || !metadata) {
return (
let innerContent = null;
if ((isResolvingUri && !claim) || !claim) {
innerContent = (
<View style={filePageStyle.container}>
<Text style={filePageStyle.emptyClaimText}>Empty claim or metadata info.</Text>
{isResolvingUri &&
<View style={filePageStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} />
<Text style={filePageStyle.infoText}>Loading decentralized data...</Text>
</View>}
{ claim === null && !isResolvingUri &&
<View style={filePageStyle.container}>
<Text style={filePageStyle.emptyClaimText}>There's nothing at this location.</Text>
</View>
}
<UriBar value={uri} navigation={navigation} />
</View>
);
} else if (claim && claim.name.length && claim.name[0] === '@') {
innerContent = (
<ChannelPage uri={uri} navigation={navigation} />
);
} else if (claim) {
const completed = fileInfo && fileInfo.completed;
const title = metadata.title;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const description = metadata.description ? metadata.description : null;
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim;
const showActions = !this.state.fullscreenMode &&
(completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
const playerStyle = [filePageStyle.player, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer];
const playerBgStyle = [filePageStyle.playerBackground, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayerBackground : filePageStyle.containedPlayerBackground];
// 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
innerContent = (
<View style={filePageStyle.pageContainer}>
<View style={filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !canLoadMedia)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{!completed && !canLoadMedia && <FileDownloadButton uri={uri} style={filePageStyle.downloadButton} />}
</View>
{canLoadMedia && <View style={playerBgStyle} />}
{canLoadMedia && <MediaPlayer fileInfo={fileInfo}
uri={uri}
style={playerStyle}
onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>}
{ showActions &&
<View style={filePageStyle.actions}>
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}
{!completed && fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes &&
<Button color="red" title="Stop Download" onPress={this.onStopDownloadPressed} />
}
</View>}
<ScrollView style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}>
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
{channelName && <Text style={filePageStyle.channelName} selectable={true}>{channelName}</Text>}
{description && <Text style={filePageStyle.description} selectable={true}>{description}</Text>}
</ScrollView>
<UriBar value={uri} navigation={navigation} />
</View>
);
}
const completed = fileInfo && fileInfo.completed;
const title = metadata.title;
const isRewardContent = rewardedContentClaimIds.includes(claim.claim_id);
const description = metadata.description ? metadata.description : null;
const mediaType = Lbry.getMediaType(contentType);
const isPlayable = mediaType === 'video' || mediaType === 'audio';
const { height, channel_name: channelName, value } = claim;
const showActions = !this.state.fullscreenMode &&
(completed || (fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes));
const channelClaimId =
value && value.publisherSignature && value.publisherSignature.certificateId;
const playerStyle = [filePageStyle.player, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayer : filePageStyle.containedPlayer];
const playerBgStyle = [filePageStyle.playerBackground, this.state.fullscreenMode ?
filePageStyle.fullscreenPlayerBackground : filePageStyle.containedPlayerBackground];
// 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
return (
<View style={filePageStyle.pageContainer}>
<View style={filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !canLoadMedia)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{!completed && !canLoadMedia && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />}
</View>
{canLoadMedia && <View style={playerBgStyle} />}
{canLoadMedia && <MediaPlayer fileInfo={fileInfo}
uri={navigation.state.params.uri}
style={playerStyle}
onFullscreenToggled={this.handleFullscreenToggle}
onMediaLoaded={() => { this.setState({ mediaLoaded: true }); }}/>}
{ showActions &&
<View style={filePageStyle.actions}>
{completed && <Button color="red" title="Delete" onPress={this.onDeletePressed} />}
{!completed && fileInfo && !fileInfo.stopped && fileInfo.written_bytes < fileInfo.total_bytes &&
<Button color="red" title="Stop Download" onPress={this.onStopDownloadPressed} />
}
</View>}
<ScrollView style={showActions ? filePageStyle.scrollContainerActions : filePageStyle.scrollContainer}>
<Text style={filePageStyle.title} selectable={true}>{title}</Text>
{channelName && <Text style={filePageStyle.channelName} selectable={true}>{channelName}</Text>}
{description && <Text style={filePageStyle.description} selectable={true}>{description}</Text>}
</ScrollView>
<View style={filePageStyle.uriContainer}>
<TextInput style={filePageStyle.uriText}
underlineColorAndroid={'transparent'}
numberOfLines={1}
value={navigation.state.params.uri} />
</View>
</View>
);
return innerContent;
}
}

View file

@ -0,0 +1,20 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const channelPageStyle = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
},
content: {
flex: 1
},
title: {
color: Colors.LbryGreen,
fontFamily: 'Metropolis-SemiBold',
fontSize: 30,
margin: 16
}
});
export default channelPageStyle;

View file

@ -2,17 +2,22 @@ import { StyleSheet } from 'react-native';
const discoverStyle = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
flex: 1
},
scrollContainer: {
flex: 1
},
busyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
title: {
fontFamily: 'Metropolis-Regular',
fontSize: 20,
textAlign: 'center',
margin: 10,
marginLeft: 10
},
categoryName: {
fontFamily: 'Metropolis-Regular',

View file

@ -2,7 +2,7 @@ import { StyleSheet } from 'react-native';
const fileDownloadButtonStyle = StyleSheet.create({
container: {
width: 120,
width: 160,
height: 36,
borderRadius: 18,
justifyContent: 'center',

View file

@ -116,19 +116,17 @@ const filePageStyle = StyleSheet.create({
position: 'absolute',
top: '40%'
},
uriContainer: {
padding: 8,
backgroundColor: Colors.VeryLightGrey,
alignSelf: 'flex-end'
busyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row'
},
uriText: {
backgroundColor: Colors.White,
borderWidth: 1,
borderColor: Colors.LightGrey,
padding: 8,
borderRadius: 4,
infoText: {
fontFamily: 'Metropolis-Regular',
fontSize: 16
fontSize: 20,
textAlign: 'center',
marginLeft: 10
}
});

30
app/src/styles/uriBar.js Normal file
View file

@ -0,0 +1,30 @@
import { StyleSheet } from 'react-native';
import Colors from './colors';
const uriBarStyle = StyleSheet.create({
uriContainer: {
backgroundColor: Colors.White,
padding: 8,
alignSelf: 'flex-end',
width: '100%',
shadowColor: Colors.Black,
shadowOpacity: 0.1,
shadowRadius: StyleSheet.hairlineWidth,
shadowOffset: {
height: StyleSheet.hairlineWidth,
},
elevation: 4
},
uriText: {
backgroundColor: Colors.White,
borderWidth: 1,
borderColor: Colors.LightGrey,
padding: 8,
borderRadius: 4,
fontFamily: 'Metropolis-Regular',
fontSize: 16,
width: '100%'
}
});
export default uriBarStyle;