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 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
|
||||||
|
|
|
@ -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, '')
|
||||||
|
|
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,
|
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}
|
||||||
|
|
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'
|
color: '#0c604b'
|
||||||
},
|
},
|
||||||
drawerHamburger: {
|
drawerHamburger: {
|
||||||
marginLeft: 8
|
marginLeft: 16
|
||||||
|
},
|
||||||
|
rightHeaderIcon: {
|
||||||
|
marginRight: 16
|
||||||
},
|
},
|
||||||
overlay: {
|
overlay: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
|
@ -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
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