diff --git a/app/src/component/AppNavigator.js b/app/src/component/AppNavigator.js index 893bd259..f6fdf295 100644 --- a/app/src/component/AppNavigator.js +++ b/app/src/component/AppNavigator.js @@ -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: navigation.navigate('DrawerOpen')} /> + headerLeft: navigation.navigate('DrawerOpen')} />, + headerRight: navigation.navigate('Search')} /> }) }, File: { @@ -26,6 +35,13 @@ const discoverStack = StackNavigator({ header: null, drawerLockMode: 'locked-closed' } + }, + Search: { + screen: SearchPage, + navigationOptions: ({ navigation }) => ({ + headerTitle: , + headerRight: 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 diff --git a/app/src/component/fileItemMedia/view.js b/app/src/component/fileItemMedia/view.js index 36201f01..d5159c38 100644 --- a/app/src/component/fileItemMedia/view.js +++ b/app/src/component/fileItemMedia/view.js @@ -45,7 +45,7 @@ class FileItemMedia extends React.PureComponent { } return ( - + {title && title .replace(/\s+/g, '') diff --git a/app/src/component/searchInput/index.js b/app/src/component/searchInput/index.js new file mode 100644 index 00000000..baffcd6b --- /dev/null +++ b/app/src/component/searchInput/index.js @@ -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); diff --git a/app/src/component/searchInput/view.js b/app/src/component/searchInput/view.js new file mode 100644 index 00000000..e59b09c3 --- /dev/null +++ b/app/src/component/searchInput/view.js @@ -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 ( + this.handleChangeText(text)} /> + ); + } +} + +export default SearchInput; diff --git a/app/src/component/searchResultItem/index.js b/app/src/component/searchResultItem/index.js new file mode 100644 index 00000000..e4b7d15d --- /dev/null +++ b/app/src/component/searchResultItem/index.js @@ -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); diff --git a/app/src/component/searchResultItem/view.js b/app/src/component/searchResultItem/view.js new file mode 100644 index 00000000..7d805a64 --- /dev/null +++ b/app/src/component/searchResultItem/view.js @@ -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 ( + + + + + + + {isResolvingUri && Loading...} + {!isResolvingUri && {title || name}} + {!isResolvingUri && channel && {channel}} + + + {obscureNsfw && navigation.navigate('Settings')} />} + + ); + } +} + +export default SearchResultItem; diff --git a/app/src/page/file/view.js b/app/src/page/file/view.js index 3bf6955b..f0427c1b 100644 --- a/app/src/page/file/view.js +++ b/app/src/page/file/view.js @@ -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 { {(!fileInfo || (isPlayable && !this.state.mediaLoaded)) && } - {isPlayable && !this.state.mediaLoaded && } + {isPlayable && !this.state.mediaLoaded && } {!completed && } {fileInfo && isPlayable && ({ + 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); diff --git a/app/src/page/search/view.js b/app/src/page/search/view.js new file mode 100644 index 00000000..3cdb8ab0 --- /dev/null +++ b/app/src/page/search/view.js @@ -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 ( + + {!isSearching && (!uris || uris.length === 0) && + No results to display.} + + {!isSearching && uris && uris.length ? ( + uris.map(uri => {navigation.navigate('File', { uri: uri }); }}/>) + ) : null } + + {isSearching && } + + ); + } +} + +export default SearchPage; diff --git a/app/src/styles/colors.js b/app/src/styles/colors.js new file mode 100644 index 00000000..1858a037 --- /dev/null +++ b/app/src/styles/colors.js @@ -0,0 +1,6 @@ +const Colors = { + LbryGreen: '#40b89a' +}; + +export default Colors; + diff --git a/app/src/styles/discover.js b/app/src/styles/discover.js index d326cd5e..0bed76ad 100644 --- a/app/src/styles/discover.js +++ b/app/src/styles/discover.js @@ -55,7 +55,10 @@ const discoverStyle = StyleSheet.create({ color: '#0c604b' }, drawerHamburger: { - marginLeft: 8 + marginLeft: 16 + }, + rightHeaderIcon: { + marginRight: 16 }, overlay: { flex: 1, diff --git a/app/src/styles/filePage.js b/app/src/styles/filePage.js index c321d4e3..8daaaafa 100644 --- a/app/src/styles/filePage.js +++ b/app/src/styles/filePage.js @@ -58,7 +58,9 @@ const filePageStyle = StyleSheet.create({ }, thumbnail: { width: screenWidth, - height: 204 + height: 204, + justifyContent: 'center', + alignItems: 'center' }, downloadButton: { position: 'absolute', diff --git a/app/src/styles/search.js b/app/src/styles/search.js new file mode 100644 index 00000000..4ad23e73 --- /dev/null +++ b/app/src/styles/search.js @@ -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;