2015-02-20 04:10:52 +00:00
|
|
|
/**
|
2015-03-28 05:18:47 +00:00
|
|
|
* The examples provided by Facebook are for non-commercial testing and
|
|
|
|
* evaluation purposes only.
|
2015-03-23 20:35:08 +00:00
|
|
|
*
|
2015-03-28 05:18:47 +00:00
|
|
|
* Facebook reserves all rights not expressly granted.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
|
|
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
|
|
|
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
|
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
2015-03-23 20:35:08 +00:00
|
|
|
*
|
2015-02-20 04:10:52 +00:00
|
|
|
* @flow
|
|
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
var React = require('react-native');
|
|
|
|
var {
|
|
|
|
ActivityIndicatorIOS,
|
2015-03-28 05:18:47 +00:00
|
|
|
ListView,
|
2015-02-20 04:10:52 +00:00
|
|
|
StyleSheet,
|
|
|
|
Text,
|
|
|
|
TextInput,
|
|
|
|
View,
|
|
|
|
} = React;
|
2015-03-24 18:35:59 +00:00
|
|
|
var TimerMixin = require('react-timer-mixin');
|
2015-02-20 04:10:52 +00:00
|
|
|
|
2015-07-01 19:51:59 +00:00
|
|
|
var invariant = require('invariant');
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
var MovieCell = require('./MovieCell');
|
|
|
|
var MovieScreen = require('./MovieScreen');
|
|
|
|
|
2015-03-27 23:16:13 +00:00
|
|
|
/**
|
|
|
|
* This is for demo purposes only, and rate limited.
|
|
|
|
* In case you want to use the Rotten Tomatoes' API on a real app you should
|
|
|
|
* create an account at http://developer.rottentomatoes.com/
|
|
|
|
*/
|
2015-02-20 04:10:52 +00:00
|
|
|
var API_URL = 'http://api.rottentomatoes.com/api/public/v1.0/';
|
2015-03-27 23:16:13 +00:00
|
|
|
var API_KEYS = [
|
|
|
|
'7waqfqbprs7pajbz28mqf6vz',
|
|
|
|
// 'y4vwv8m33hed9ety83jmv52f', Fallback api_key
|
|
|
|
];
|
2015-02-20 04:10:52 +00:00
|
|
|
|
|
|
|
// Results should be cached keyed by the query
|
|
|
|
// with values of null meaning "being fetched"
|
|
|
|
// and anything besides null and undefined
|
|
|
|
// as the result of a valid query
|
|
|
|
var resultsCache = {
|
|
|
|
dataForQuery: {},
|
|
|
|
nextPageNumberForQuery: {},
|
|
|
|
totalForQuery: {},
|
|
|
|
};
|
|
|
|
|
|
|
|
var LOADING = {};
|
|
|
|
|
|
|
|
var SearchScreen = React.createClass({
|
|
|
|
mixins: [TimerMixin],
|
|
|
|
|
2015-03-21 01:33:06 +00:00
|
|
|
timeoutID: (null: any),
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
getInitialState: function() {
|
|
|
|
return {
|
|
|
|
isLoading: false,
|
|
|
|
isLoadingTail: false,
|
2015-03-03 00:02:42 +00:00
|
|
|
dataSource: new ListView.DataSource({
|
2015-02-20 04:10:52 +00:00
|
|
|
rowHasChanged: (row1, row2) => row1 !== row2,
|
|
|
|
}),
|
|
|
|
filter: '',
|
|
|
|
queryNumber: 0,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
|
|
|
|
componentDidMount: function() {
|
|
|
|
this.searchMovies('');
|
|
|
|
},
|
|
|
|
|
2015-07-01 19:51:59 +00:00
|
|
|
_urlForQueryAndPage: function(query: string, pageNumber: number): string {
|
2015-02-20 04:10:52 +00:00
|
|
|
var apiKey = API_KEYS[this.state.queryNumber % API_KEYS.length];
|
|
|
|
if (query) {
|
|
|
|
return (
|
|
|
|
API_URL + 'movies.json?apikey=' + apiKey + '&q=' +
|
|
|
|
encodeURIComponent(query) + '&page_limit=20&page=' + pageNumber
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
// With no query, load latest movies
|
|
|
|
return (
|
|
|
|
API_URL + 'lists/movies/in_theaters.json?apikey=' + apiKey +
|
|
|
|
'&page_limit=20&page=' + pageNumber
|
|
|
|
);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
searchMovies: function(query: string) {
|
|
|
|
this.timeoutID = null;
|
|
|
|
|
|
|
|
this.setState({filter: query});
|
|
|
|
|
|
|
|
var cachedResultsForQuery = resultsCache.dataForQuery[query];
|
|
|
|
if (cachedResultsForQuery) {
|
|
|
|
if (!LOADING[query]) {
|
|
|
|
this.setState({
|
|
|
|
dataSource: this.getDataSource(cachedResultsForQuery),
|
|
|
|
isLoading: false
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.setState({isLoading: true});
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOADING[query] = true;
|
|
|
|
resultsCache.dataForQuery[query] = null;
|
|
|
|
this.setState({
|
|
|
|
isLoading: true,
|
|
|
|
queryNumber: this.state.queryNumber + 1,
|
|
|
|
isLoadingTail: false,
|
|
|
|
});
|
|
|
|
|
|
|
|
fetch(this._urlForQueryAndPage(query, 1))
|
|
|
|
.then((response) => response.json())
|
2015-03-13 00:06:18 +00:00
|
|
|
.catch((error) => {
|
|
|
|
LOADING[query] = false;
|
|
|
|
resultsCache.dataForQuery[query] = undefined;
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
dataSource: this.getDataSource([]),
|
|
|
|
isLoading: false,
|
|
|
|
});
|
|
|
|
})
|
2015-02-20 04:10:52 +00:00
|
|
|
.then((responseData) => {
|
|
|
|
LOADING[query] = false;
|
|
|
|
resultsCache.totalForQuery[query] = responseData.total;
|
|
|
|
resultsCache.dataForQuery[query] = responseData.movies;
|
|
|
|
resultsCache.nextPageNumberForQuery[query] = 2;
|
|
|
|
|
|
|
|
if (this.state.filter !== query) {
|
|
|
|
// do not update state if the query is stale
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
isLoading: false,
|
|
|
|
dataSource: this.getDataSource(responseData.movies),
|
|
|
|
});
|
|
|
|
})
|
2015-03-13 00:06:18 +00:00
|
|
|
.done();
|
2015-02-20 04:10:52 +00:00
|
|
|
},
|
|
|
|
|
|
|
|
hasMore: function(): boolean {
|
|
|
|
var query = this.state.filter;
|
|
|
|
if (!resultsCache.dataForQuery[query]) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
resultsCache.totalForQuery[query] !==
|
|
|
|
resultsCache.dataForQuery[query].length
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
onEndReached: function() {
|
|
|
|
var query = this.state.filter;
|
|
|
|
if (!this.hasMore() || this.state.isLoadingTail) {
|
|
|
|
// We're already fetching or have all the elements so noop
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (LOADING[query]) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
LOADING[query] = true;
|
|
|
|
this.setState({
|
|
|
|
queryNumber: this.state.queryNumber + 1,
|
|
|
|
isLoadingTail: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
var page = resultsCache.nextPageNumberForQuery[query];
|
2015-07-01 19:51:59 +00:00
|
|
|
invariant(page != null, 'Next page number for "%s" is missing', query);
|
2015-02-20 04:10:52 +00:00
|
|
|
fetch(this._urlForQueryAndPage(query, page))
|
|
|
|
.then((response) => response.json())
|
2015-03-13 00:06:18 +00:00
|
|
|
.catch((error) => {
|
|
|
|
console.error(error);
|
|
|
|
LOADING[query] = false;
|
|
|
|
this.setState({
|
|
|
|
isLoadingTail: false,
|
|
|
|
});
|
|
|
|
})
|
2015-02-20 04:10:52 +00:00
|
|
|
.then((responseData) => {
|
|
|
|
var moviesForQuery = resultsCache.dataForQuery[query].slice();
|
|
|
|
|
|
|
|
LOADING[query] = false;
|
|
|
|
// We reached the end of the list before the expected number of results
|
|
|
|
if (!responseData.movies) {
|
|
|
|
resultsCache.totalForQuery[query] = moviesForQuery.length;
|
|
|
|
} else {
|
|
|
|
for (var i in responseData.movies) {
|
|
|
|
moviesForQuery.push(responseData.movies[i]);
|
|
|
|
}
|
|
|
|
resultsCache.dataForQuery[query] = moviesForQuery;
|
|
|
|
resultsCache.nextPageNumberForQuery[query] += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.state.filter !== query) {
|
|
|
|
// do not update state if the query is stale
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.setState({
|
|
|
|
isLoadingTail: false,
|
|
|
|
dataSource: this.getDataSource(resultsCache.dataForQuery[query]),
|
|
|
|
});
|
|
|
|
})
|
2015-03-13 00:06:18 +00:00
|
|
|
.done();
|
2015-02-20 04:10:52 +00:00
|
|
|
},
|
|
|
|
|
2015-03-03 00:02:42 +00:00
|
|
|
getDataSource: function(movies: Array<any>): ListView.DataSource {
|
2015-02-20 04:10:52 +00:00
|
|
|
return this.state.dataSource.cloneWithRows(movies);
|
|
|
|
},
|
|
|
|
|
|
|
|
selectMovie: function(movie: Object) {
|
|
|
|
this.props.navigator.push({
|
|
|
|
title: movie.title,
|
|
|
|
component: MovieScreen,
|
|
|
|
passProps: {movie},
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
onSearchChange: function(event: Object) {
|
|
|
|
var filter = event.nativeEvent.text.toLowerCase();
|
|
|
|
|
|
|
|
this.clearTimeout(this.timeoutID);
|
|
|
|
this.timeoutID = this.setTimeout(() => this.searchMovies(filter), 100);
|
|
|
|
},
|
|
|
|
|
|
|
|
renderFooter: function() {
|
|
|
|
if (!this.hasMore() || !this.state.isLoadingTail) {
|
|
|
|
return <View style={styles.scrollSpinner} />;
|
|
|
|
}
|
|
|
|
return <ActivityIndicatorIOS style={styles.scrollSpinner} />;
|
|
|
|
},
|
|
|
|
|
2015-05-26 22:16:42 +00:00
|
|
|
renderSeparator: function(
|
|
|
|
sectionID: number | string,
|
|
|
|
rowID: number | string,
|
|
|
|
adjacentRowHighlighted: boolean
|
|
|
|
) {
|
|
|
|
var style = styles.rowSeparator;
|
|
|
|
if (adjacentRowHighlighted) {
|
|
|
|
style = [style, styles.rowSeparatorHide];
|
|
|
|
}
|
|
|
|
return (
|
|
|
|
<View key={"SEP_" + sectionID + "_" + rowID} style={style}/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
renderRow: function(
|
|
|
|
movie: Object,
|
|
|
|
sectionID: number | string,
|
|
|
|
rowID: number | string,
|
|
|
|
highlightRowFunc: (sectionID: ?number | string, rowID: ?number | string) => void,
|
|
|
|
) {
|
2015-02-20 04:10:52 +00:00
|
|
|
return (
|
|
|
|
<MovieCell
|
|
|
|
onSelect={() => this.selectMovie(movie)}
|
2015-05-26 22:16:42 +00:00
|
|
|
onHighlight={() => highlightRowFunc(sectionID, rowID)}
|
|
|
|
onUnhighlight={() => highlightRowFunc(null, null)}
|
2015-02-20 04:10:52 +00:00
|
|
|
movie={movie}
|
|
|
|
/>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
|
|
|
|
render: function() {
|
|
|
|
var content = this.state.dataSource.getRowCount() === 0 ?
|
|
|
|
<NoMovies
|
|
|
|
filter={this.state.filter}
|
|
|
|
isLoading={this.state.isLoading}
|
|
|
|
/> :
|
|
|
|
<ListView
|
|
|
|
ref="listview"
|
2015-05-26 22:16:42 +00:00
|
|
|
renderSeparator={this.renderSeparator}
|
2015-02-20 04:10:52 +00:00
|
|
|
dataSource={this.state.dataSource}
|
|
|
|
renderFooter={this.renderFooter}
|
|
|
|
renderRow={this.renderRow}
|
|
|
|
onEndReached={this.onEndReached}
|
|
|
|
automaticallyAdjustContentInsets={false}
|
2015-06-05 15:46:17 +00:00
|
|
|
keyboardDismissMode="on-drag"
|
2015-02-20 04:10:52 +00:00
|
|
|
keyboardShouldPersistTaps={true}
|
|
|
|
showsVerticalScrollIndicator={false}
|
|
|
|
/>;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<View style={styles.container}>
|
|
|
|
<SearchBar
|
|
|
|
onSearchChange={this.onSearchChange}
|
|
|
|
isLoading={this.state.isLoading}
|
|
|
|
onFocus={() => this.refs.listview.getScrollResponder().scrollTo(0, 0)}
|
|
|
|
/>
|
|
|
|
<View style={styles.separator} />
|
|
|
|
{content}
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
});
|
|
|
|
|
|
|
|
var NoMovies = React.createClass({
|
|
|
|
render: function() {
|
|
|
|
var text = '';
|
|
|
|
if (this.props.filter) {
|
|
|
|
text = `No results for “${this.props.filter}”`;
|
|
|
|
} else if (!this.props.isLoading) {
|
|
|
|
// If we're looking at the latest movies, aren't currently loading, and
|
|
|
|
// still have no results, show a message
|
|
|
|
text = 'No movies found';
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<View style={[styles.container, styles.centerText]}>
|
|
|
|
<Text style={styles.noMoviesText}>{text}</Text>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var SearchBar = React.createClass({
|
|
|
|
render: function() {
|
|
|
|
return (
|
|
|
|
<View style={styles.searchBar}>
|
|
|
|
<TextInput
|
2015-03-04 22:04:52 +00:00
|
|
|
autoCapitalize="none"
|
2015-02-20 04:10:52 +00:00
|
|
|
autoCorrect={false}
|
|
|
|
onChange={this.props.onSearchChange}
|
|
|
|
placeholder="Search a movie..."
|
|
|
|
onFocus={this.props.onFocus}
|
|
|
|
style={styles.searchBarInput}
|
|
|
|
/>
|
|
|
|
<ActivityIndicatorIOS
|
|
|
|
animating={this.props.isLoading}
|
|
|
|
style={styles.spinner}
|
|
|
|
/>
|
|
|
|
</View>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
var styles = StyleSheet.create({
|
|
|
|
container: {
|
|
|
|
flex: 1,
|
|
|
|
backgroundColor: 'white',
|
|
|
|
},
|
|
|
|
centerText: {
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
noMoviesText: {
|
|
|
|
marginTop: 80,
|
|
|
|
color: '#888888',
|
|
|
|
},
|
|
|
|
searchBar: {
|
|
|
|
marginTop: 64,
|
|
|
|
padding: 3,
|
|
|
|
paddingLeft: 8,
|
|
|
|
flexDirection: 'row',
|
|
|
|
alignItems: 'center',
|
|
|
|
},
|
|
|
|
searchBarInput: {
|
|
|
|
fontSize: 15,
|
|
|
|
flex: 1,
|
|
|
|
height: 30,
|
|
|
|
},
|
|
|
|
separator: {
|
|
|
|
height: 1,
|
|
|
|
backgroundColor: '#eeeeee',
|
|
|
|
},
|
|
|
|
spinner: {
|
|
|
|
width: 30,
|
|
|
|
},
|
|
|
|
scrollSpinner: {
|
|
|
|
marginVertical: 20,
|
|
|
|
},
|
2015-05-26 22:16:42 +00:00
|
|
|
rowSeparator: {
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.1)',
|
|
|
|
height: 1,
|
|
|
|
marginLeft: 4,
|
|
|
|
},
|
|
|
|
rowSeparatorHide: {
|
|
|
|
opacity: 0.0,
|
|
|
|
},
|
2015-02-20 04:10:52 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
module.exports = SearchScreen;
|