Search result item component and search page (#60)

* Implemented SearchResultItem component and search page
This commit is contained in:
akinwale 2018-04-09 14:39:35 -04:00 committed by GitHub
parent 0f454b562d
commit 1e32b6e875
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 286 additions and 9 deletions

View file

@ -1,23 +1,32 @@
import React from 'react'; import React from 'react';
import DiscoverPage from '../page/discover'; import DiscoverPage from '../page/discover';
import FilePage from '../page/file'; import FilePage from '../page/file';
import SearchPage from '../page/search';
import SettingsPage from '../page/settings'; import SettingsPage from '../page/settings';
import SplashScreen from '../page/splash'; import SplashScreen from '../page/splash';
import { addNavigationHelpers, DrawerNavigator, StackNavigator } from 'react-navigation'; import SearchInput from '../component/searchInput';
import {
addNavigationHelpers,
DrawerNavigator,
StackNavigator,
NavigationActions
} from 'react-navigation';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { addListener } from '../utils/redux'; import { addListener } from '../utils/redux';
import { AppState, BackHandler, NativeModules } from 'react-native'; import { AppState, BackHandler, NativeModules, TextInput } from 'react-native';
import { SETTINGS } from 'lbry-redux'; import { SETTINGS } from 'lbry-redux';
import { makeSelectClientSetting } from '../redux/selectors/settings';
import Feather from 'react-native-vector-icons/Feather'; import Feather from 'react-native-vector-icons/Feather';
import discoverStyle from '../styles/discover'; import discoverStyle from '../styles/discover';
import { makeSelectClientSetting } from '../redux/selectors/settings'; import searchStyle from '../styles/search';
const discoverStack = StackNavigator({ const discoverStack = StackNavigator({
Discover: { Discover: {
screen: DiscoverPage, screen: DiscoverPage,
navigationOptions: ({ navigation }) => ({ navigationOptions: ({ navigation }) => ({
title: 'Discover', title: 'Discover',
headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} /> headerLeft: <Feather name="menu" size={24} style={discoverStyle.drawerHamburger} onPress={() => navigation.navigate('DrawerOpen')} />,
headerRight: <Feather name="search" size={24} style={discoverStyle.rightHeaderIcon} onPress={() => navigation.navigate('Search')} />
}) })
}, },
File: { File: {
@ -26,6 +35,13 @@ const discoverStack = StackNavigator({
header: null, header: null,
drawerLockMode: 'locked-closed' drawerLockMode: 'locked-closed'
} }
},
Search: {
screen: SearchPage,
navigationOptions: ({ navigation }) => ({
headerTitle: <SearchInput style={searchStyle.searchInput} />,
headerRight: <Feather name="x" size={24} style={discoverStyle.rightHeaderIcon} onPress={() => navigation.dispatch(NavigationActions.back())} />
})
} }
}, { }, {
headerMode: 'screen', headerMode: 'screen',
@ -44,7 +60,10 @@ const drawer = DrawerNavigator({
export const AppNavigator = new StackNavigator({ export const AppNavigator = new StackNavigator({
Splash: { Splash: {
screen: SplashScreen screen: SplashScreen,
navigationOptions: {
drawerLockMode: 'locked-closed'
}
}, },
Main: { Main: {
screen: drawer screen: drawer

View file

@ -45,7 +45,7 @@ class FileItemMedia extends React.PureComponent {
} }
return ( return (
<View style={[fileItemMediaStyle.autothumb, atStyle]}> <View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
<Text style={fileItemMediaStyle.autothumbText}>{title && <Text style={fileItemMediaStyle.autothumbText}>{title &&
title title
.replace(/\s+/g, '') .replace(/\s+/g, '')

View file

@ -0,0 +1,10 @@
import { connect } from 'react-redux';
import { doSearch, doUpdateSearchQuery } from 'lbry-redux';
import SearchInput from './view';
const perform = dispatch => ({
search: search => dispatch(doSearch(search)),
updateSearchQuery: query => dispatch(doUpdateSearchQuery(query, false))
});
export default connect(null, perform)(SearchInput);

View file

@ -0,0 +1,40 @@
import React from 'react';
import { TextInput } from 'react-native';
class SearchInput extends React.PureComponent {
static INPUT_TIMEOUT = 500;
state = {
changeTextTimeout: -1
};
handleChangeText = text => {
clearTimeout(this.state.changeTextTimeout);
if (!text || text.trim().length < 2) {
// only perform a search if 2 or more characters have been input
return;
}
const { search, updateSearchQuery } = this.props;
updateSearchQuery(text);
let timeout = setTimeout(() => {
search(text);
}, SearchInput.INPUT_TIMEOUT);
this.setState({ changeTextTimeout: timeout });
}
render() {
const { style, value } = this.props;
return (
<TextInput
style={style}
placeholder="Search"
underlineColorAndroid="transparent"
value={value}
onChangeText={text => this.handleChangeText(text)} />
);
}
}
export default SearchInput;

View file

@ -0,0 +1,24 @@
import { connect } from 'react-redux';
import {
doResolveUri,
makeSelectClaimForUri,
makeSelectMetadataForUri,
makeSelectFileInfoForUri,
makeSelectIsUriResolving,
} from 'lbry-redux';
import { selectShowNsfw } from '../../redux/selectors/settings';
import SearchResultItem from './view';
const select = (state, props) => ({
claim: makeSelectClaimForUri(props.uri)(state),
isDownloaded: !!makeSelectFileInfoForUri(props.uri)(state),
metadata: makeSelectMetadataForUri(props.uri)(state),
isResolvingUri: makeSelectIsUriResolving(props.uri)(state),
obscureNsfw: !selectShowNsfw(state)
});
const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri))
});
export default connect(select, perform)(SearchResultItem);

View file

@ -0,0 +1,53 @@
import React from 'react';
import { normalizeURI, parseURI } from 'lbry-redux';
import { Text, TouchableOpacity, View } from 'react-native';
import FileItemMedia from '../fileItemMedia';
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}>
<View style={searchStyle.thumbnailContainer}>
<FileItemMedia style={searchStyle.thumbnail}
blurRadius={obscureNsfw ? 15 : 0}
title={title}
thumbnail={metadata ? metadata.thumbnail : null} />
</View>
<View style={searchStyle.detailsContainer}>
{isResolvingUri && <Text style={searchStyle.loading}>Loading...</Text>}
{!isResolvingUri && <Text style={searchStyle.title}>{title || name}</Text>}
{!isResolvingUri && channel && <Text style={searchStyle.publisher}>{channel}</Text>}
</View>
</TouchableOpacity>
{obscureNsfw && <NsfwOverlay onPress={() => navigation.navigate('Settings')} />}
</View>
);
}
}
export default SearchResultItem;

View file

@ -11,6 +11,7 @@ import {
TouchableOpacity, TouchableOpacity,
NativeModules NativeModules
} from 'react-native'; } from 'react-native';
import Colors from '../../styles/colors';
import FileItemMedia from '../../component/fileItemMedia'; import FileItemMedia from '../../component/fileItemMedia';
import FileDownloadButton from '../../component/fileDownloadButton'; import FileDownloadButton from '../../component/fileDownloadButton';
import MediaPlayer from '../../component/mediaPlayer'; import MediaPlayer from '../../component/mediaPlayer';
@ -132,7 +133,7 @@ class FilePage extends React.PureComponent {
<View style={this.state.fullscreenMode ? filePageStyle.fullscreenMedia : filePageStyle.mediaContainer}> <View style={this.state.fullscreenMode ? filePageStyle.fullscreenMedia : filePageStyle.mediaContainer}>
{(!fileInfo || (isPlayable && !this.state.mediaLoaded)) && {(!fileInfo || (isPlayable && !this.state.mediaLoaded)) &&
<FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />} <FileItemMedia style={filePageStyle.thumbnail} title={title} thumbnail={metadata.thumbnail} />}
{isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color="#40b89a" style={filePageStyle.loading} />} {isPlayable && !this.state.mediaLoaded && <ActivityIndicator size="large" color={Colors.LbryGreen} style={filePageStyle.loading} />}
{!completed && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />} {!completed && <FileDownloadButton uri={navigation.state.params.uri} style={filePageStyle.downloadButton} />}
{fileInfo && isPlayable && <MediaPlayer fileInfo={fileInfo} {fileInfo && isPlayable && <MediaPlayer fileInfo={fileInfo}
style={filePageStyle.player} style={filePageStyle.player}

View file

@ -0,0 +1,20 @@
import { connect } from 'react-redux';
import {
doSearch,
makeSelectSearchUris,
selectIsSearching,
selectSearchValue
} from 'lbry-redux';
import SearchPage from './view';
const select = (state) => ({
isSearching: selectIsSearching(state),
query: selectSearchValue(state),
uris: makeSelectSearchUris(selectSearchValue(state))(state)
});
const perform = dispatch => ({
search: (query) => dispatch(doSearch(query)),
});
export default connect(select, perform)(SearchPage);

View file

@ -0,0 +1,38 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
Button,
Text,
TextInput,
View,
ScrollView
} from 'react-native';
import SearchResultItem from '../../component/searchResultItem';
import Colors from '../../styles/colors';
import searchStyle from '../../styles/search';
class SearchPage extends React.PureComponent {
render() {
const { isSearching, navigation, uris } = this.props;
return (
<View style={searchStyle.container}>
{!isSearching && (!uris || uris.length === 0) &&
<Text style={searchStyle.noResultsText}>No results to display.</Text>}
<ScrollView style={searchStyle.scrollContainer} contentContainerStyle={searchStyle.scrollPadding}>
{!isSearching && uris && uris.length ? (
uris.map(uri => <SearchResultItem key={uri}
uri={uri}
style={searchStyle.resultItem}
navigation={navigation}
onPress={() => {navigation.navigate('File', { uri: uri }); }}/>)
) : null }
</ScrollView>
{isSearching && <ActivityIndicator size="large" color={Colors.LbryGreen} style={searchStyle.loading} /> }
</View>
);
}
}
export default SearchPage;

6
app/src/styles/colors.js Normal file
View file

@ -0,0 +1,6 @@
const Colors = {
LbryGreen: '#40b89a'
};
export default Colors;

View file

@ -55,7 +55,10 @@ const discoverStyle = StyleSheet.create({
color: '#0c604b' color: '#0c604b'
}, },
drawerHamburger: { drawerHamburger: {
marginLeft: 8 marginLeft: 16
},
rightHeaderIcon: {
marginRight: 16
}, },
overlay: { overlay: {
flex: 1, flex: 1,

View file

@ -58,7 +58,9 @@ const filePageStyle = StyleSheet.create({
}, },
thumbnail: { thumbnail: {
width: screenWidth, width: screenWidth,
height: 204 height: 204,
justifyContent: 'center',
alignItems: 'center'
}, },
downloadButton: { downloadButton: {
position: 'absolute', position: 'absolute',

61
app/src/styles/search.js Normal file
View file

@ -0,0 +1,61 @@
import { StyleSheet } from 'react-native';
const searchStyle = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
scrollContainer: {
flex: 1,
width: '100%',
height: '100%',
padding: 16
},
scrollPadding: {
paddingBottom: 16
},
resultItem: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16
},
thumbnail: {
width: '100%',
height: 80
},
thumbnailContainer: {
width: '25%'
},
detailsContainer: {
width: '70%'
},
searchInput: {
width: '100%',
height: '100%',
fontFamily: 'Metropolis-Regular',
fontSize: 16
},
title: {
fontFamily: 'Metropolis-SemiBold',
fontSize: 16
},
publisher: {
fontFamily: 'Metropolis-SemiBold',
fontSize: 12,
marginTop: 4,
color: '#c0c0c0'
},
noResultsText: {
textAlign: 'center',
fontFamily: 'Metropolis-Regular',
fontSize: 14,
position: 'absolute'
},
loading: {
position: 'absolute'
}
});
export default searchStyle;