Search result item component and search page (#60)
* Implemented SearchResultItem component and search page
This commit is contained in:
parent
0f454b562d
commit
1e32b6e875
13 changed files with 286 additions and 9 deletions
|
@ -1,23 +1,32 @@
|
|||
import React from 'react';
|
||||
import DiscoverPage from '../page/discover';
|
||||
import FilePage from '../page/file';
|
||||
import SearchPage from '../page/search';
|
||||
import SettingsPage from '../page/settings';
|
||||
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 { 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 { makeSelectClientSetting } from '../redux/selectors/settings';
|
||||
import Feather from 'react-native-vector-icons/Feather';
|
||||
import discoverStyle from '../styles/discover';
|
||||
import { makeSelectClientSetting } from '../redux/selectors/settings';
|
||||
import searchStyle from '../styles/search';
|
||||
|
||||
const discoverStack = StackNavigator({
|
||||
Discover: {
|
||||
screen: DiscoverPage,
|
||||
navigationOptions: ({ navigation }) => ({
|
||||
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: {
|
||||
|
@ -26,6 +35,13 @@ const discoverStack = StackNavigator({
|
|||
header: null,
|
||||
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',
|
||||
|
@ -44,7 +60,10 @@ const drawer = DrawerNavigator({
|
|||
|
||||
export const AppNavigator = new StackNavigator({
|
||||
Splash: {
|
||||
screen: SplashScreen
|
||||
screen: SplashScreen,
|
||||
navigationOptions: {
|
||||
drawerLockMode: 'locked-closed'
|
||||
}
|
||||
},
|
||||
Main: {
|
||||
screen: drawer
|
||||
|
|
|
@ -45,7 +45,7 @@ class FileItemMedia extends React.PureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={[fileItemMediaStyle.autothumb, atStyle]}>
|
||||
<View style={[style ? style : fileItemMediaStyle.autothumb, atStyle]}>
|
||||
<Text style={fileItemMediaStyle.autothumbText}>{title &&
|
||||
title
|
||||
.replace(/\s+/g, '')
|
||||
|
|
10
app/src/component/searchInput/index.js
Normal file
10
app/src/component/searchInput/index.js
Normal 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);
|
40
app/src/component/searchInput/view.js
Normal file
40
app/src/component/searchInput/view.js
Normal 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;
|
24
app/src/component/searchResultItem/index.js
Normal file
24
app/src/component/searchResultItem/index.js
Normal 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);
|
53
app/src/component/searchResultItem/view.js
Normal file
53
app/src/component/searchResultItem/view.js
Normal 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;
|
|
@ -11,6 +11,7 @@ import {
|
|||
TouchableOpacity,
|
||||
NativeModules
|
||||
} from 'react-native';
|
||||
import Colors from '../../styles/colors';
|
||||
import FileItemMedia from '../../component/fileItemMedia';
|
||||
import FileDownloadButton from '../../component/fileDownloadButton';
|
||||
import MediaPlayer from '../../component/mediaPlayer';
|
||||
|
@ -132,7 +133,7 @@ class FilePage extends React.PureComponent {
|
|||
<View style={this.state.fullscreenMode ? filePageStyle.fullscreenMedia : filePageStyle.mediaContainer}>
|
||||
{(!fileInfo || (isPlayable && !this.state.mediaLoaded)) &&
|
||||
<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} />}
|
||||
{fileInfo && isPlayable && <MediaPlayer fileInfo={fileInfo}
|
||||
style={filePageStyle.player}
|
||||
|
|
20
app/src/page/search/index.js
Normal file
20
app/src/page/search/index.js
Normal 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);
|
38
app/src/page/search/view.js
Normal file
38
app/src/page/search/view.js
Normal 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
6
app/src/styles/colors.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
const Colors = {
|
||||
LbryGreen: '#40b89a'
|
||||
};
|
||||
|
||||
export default Colors;
|
||||
|
|
@ -55,7 +55,10 @@ const discoverStyle = StyleSheet.create({
|
|||
color: '#0c604b'
|
||||
},
|
||||
drawerHamburger: {
|
||||
marginLeft: 8
|
||||
marginLeft: 16
|
||||
},
|
||||
rightHeaderIcon: {
|
||||
marginRight: 16
|
||||
},
|
||||
overlay: {
|
||||
flex: 1,
|
||||
|
|
|
@ -58,7 +58,9 @@ const filePageStyle = StyleSheet.create({
|
|||
},
|
||||
thumbnail: {
|
||||
width: screenWidth,
|
||||
height: 204
|
||||
height: 204,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
downloadButton: {
|
||||
position: 'absolute',
|
||||
|
|
61
app/src/styles/search.js
Normal file
61
app/src/styles/search.js
Normal 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;
|
Loading…
Reference in a new issue