PageRenderTime 27ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/mobile-examples/src/main/scala/sri/mobile/examples/movies/SearchScreen.scala

https://gitlab.com/nafg/sri
Scala | 224 lines | 176 code | 47 blank | 1 comment | 37 complexity | e268a12ce86c30f7cbe83049c0cda934 MD5 | raw file
  1. package sri.mobile.examples.movies
  2. import org.scalajs.dom
  3. import org.scalajs.dom.ext.{Ajax, AjaxException}
  4. import sri.core.{ReactComponent, ReactElement}
  5. import sri.mobile.all._
  6. import sri.mobile.components.ios.ActivityIndicatorIOS
  7. import sri.mobile.examples.movies.android.SearchBarAndroid
  8. import sri.mobile.examples.movies.ios.SearchBarIOS
  9. import sri.universal.{TextInputEvent, ReactEvent}
  10. import sri.universal.components._
  11. import sri.universal.styles.UniversalStyleSheet
  12. import scala.async.Async._
  13. import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue
  14. import scala.scalajs.js
  15. import scala.scalajs.js.`|`
  16. import scala.scalajs.js.annotation.ScalaJSDefined
  17. import scala.scalajs.js.{JSON, URIUtils, UndefOr}
  18. object SearchScreen {
  19. val API_URL = "http://api.rottentomatoes.com/api/public/v1.0/"
  20. val API_KEYS = List("7waqfqbprs7pajbz28mqf6vz", "y4vwv8m33hed9ety83jmv52f")
  21. object NoMovies {
  22. val Component = (props: Props) => {
  23. var text = ""
  24. if (props.filter.nonEmpty) text = s"No results for ${props.filter}"
  25. else if (!props.isLoading) text = "No movies found"
  26. View(style = styles.container)(
  27. Text(style = styles.noMoviesText)(text)
  28. )
  29. }
  30. case class Props(filter: String, isLoading: Boolean)
  31. def apply(filter: String, isLoading: Boolean, key: UndefOr[String] = js.undefined) = createStatelessFunctionElement(Component, Props(filter, isLoading), key = key)
  32. }
  33. val LOADING = collection.mutable.Map.empty[String, Boolean].withDefaultValue(false)
  34. case class ResultsCache(dataForQuery: Map[String, js.Array[js.Dynamic]] = Map().withDefaultValue(js.Array()), nextPageNumberForQuery: Map[String, Int] = Map().withDefaultValue(0), totalForQuery: Map[String, Int] = Map().withDefaultValue(0))
  35. case class State(isLoading: Boolean = false, isLoadingTail: Boolean = false, dataSource: ListViewDataSource[js.Dynamic, String] = createListViewDataSource((row1: js.Dynamic, row2: js.Dynamic) => row1 != row2), queryNumber: Int = 0)
  36. @ScalaJSDefined
  37. class Component extends ReactComponent[Unit, State] {
  38. initialState(State())
  39. def render() = {
  40. val content: ReactElement = if (state.dataSource.getRowCount() == 0) NoMovies(filter, state.isLoading)
  41. else ListView[js.Dynamic, String](
  42. ref = storeListViewRef _,
  43. dataSource = state.dataSource,
  44. renderRow = renderRow,
  45. onEndReached = onEndReached _,
  46. renderFooter = renderFooter _,
  47. showsVerticalScrollIndicator = false,
  48. keyboardShouldPersistTaps = true,
  49. automaticallyAdjustContentInsets = false
  50. )()
  51. View(style = styles.container)(
  52. if (isIOSPlatform)
  53. SearchBarIOS(onSearchChange, onSearchInputFocus, state.isLoading)
  54. else SearchBarAndroid(onSearchChange, onSearchInputFocus, state.isLoading),
  55. View(style = styles.separator)(),
  56. content
  57. )
  58. }
  59. override def componentDidMount(): Unit = {
  60. searchMovies("")
  61. }
  62. var resultsCache = ResultsCache()
  63. var timeoutID: Int = _
  64. var listViewMounted: ListViewM = null
  65. var filter = ""
  66. def storeListViewRef(ref: ListViewM) = {
  67. if (!js.isUndefined(ref) && ref != null) listViewMounted = ref
  68. }
  69. def _urlForQueryAndPage(query: String, pageNumber: Int) = {
  70. val apiKey = API_KEYS(state.queryNumber % API_KEYS.length)
  71. if (query.nonEmpty) s"${API_URL}movies.json?apikey=${apiKey}&q=${URIUtils.encodeURIComponent(query)}&page_limit=20&page=$pageNumber"
  72. else s"${API_URL}lists/movies/in_theaters.json?apikey=${apiKey}&page_limit=20&page=${pageNumber}"
  73. }
  74. def getDataSource(movies: js.Array[js.Dynamic]) = {
  75. state.dataSource.cloneWithRows(movies)
  76. }
  77. def searchMovies(query: String) = {
  78. filter = query
  79. val cachedResultsForQuery = resultsCache.dataForQuery.getOrElse(query, null)
  80. if (cachedResultsForQuery != null) {
  81. if (!LOADING.getOrElse(query, false)) {
  82. setState(state.copy(dataSource = getDataSource(cachedResultsForQuery), isLoading = false))
  83. } else {
  84. setState(state.copy(isLoading = true))
  85. }
  86. } else {
  87. LOADING += query -> true
  88. resultsCache = resultsCache.copy(dataForQuery = resultsCache.dataForQuery.updated(query, null))
  89. setState(state.copy(isLoading = true, queryNumber = state.queryNumber + 1, isLoadingTail = false))
  90. val page = resultsCache.nextPageNumberForQuery.getOrElse(query, 1)
  91. async {
  92. val result = await(Ajax.get(_urlForQueryAndPage(query, page)))
  93. val response = JSON.parse(result.responseText)
  94. val movies = response.movies.asInstanceOf[js.Array[js.Dynamic]]
  95. LOADING.update(query, false)
  96. resultsCache = resultsCache.copy(dataForQuery = resultsCache.dataForQuery.updated(query, movies),
  97. nextPageNumberForQuery = resultsCache.nextPageNumberForQuery.updated(query, 2),
  98. totalForQuery = resultsCache.totalForQuery.updated(query, response.total.asInstanceOf[Int]))
  99. if (filter == query) setState(state.copy(isLoading = false, dataSource = getDataSource(movies)))
  100. }.recover {
  101. case ex => {
  102. LOADING.update(query, false)
  103. setState(state.copy(isLoading = false))
  104. println(s"Error searching movies with query $query -> ${ex.asInstanceOf[AjaxException].xhr.responseText}")
  105. }
  106. }
  107. }
  108. }
  109. def hasMore = {
  110. val query = filter
  111. if (resultsCache.dataForQuery.getOrElse(query, null) == null) true
  112. else resultsCache.totalForQuery(query) != resultsCache.dataForQuery(query).length
  113. }
  114. def onEndReached: Unit = {
  115. val query = filter
  116. if (hasMore || !state.isLoadingTail || !LOADING(query)) {
  117. // if we have all elements or fetching don't do anything
  118. LOADING += query -> true
  119. setState(state.copy(queryNumber = state.queryNumber + 1, isLoadingTail = true))
  120. val page = resultsCache.nextPageNumberForQuery(query)
  121. async {
  122. val result = await(Ajax.get(_urlForQueryAndPage(query, page)))
  123. val response = JSON.parse(result.responseText)
  124. val moviesForQuery = resultsCache.dataForQuery(query)
  125. LOADING.update(query, false)
  126. if (js.isUndefined(response.movies)) {
  127. resultsCache = resultsCache.copy(totalForQuery = resultsCache.totalForQuery.updated(query, moviesForQuery.length))
  128. } else {
  129. val movies = response.movies.asInstanceOf[js.Array[js.Dynamic]]
  130. movies.foreach(m => moviesForQuery.push(m))
  131. resultsCache = resultsCache.copy(dataForQuery = resultsCache.dataForQuery.updated(query, moviesForQuery),
  132. nextPageNumberForQuery = resultsCache.nextPageNumberForQuery.updated(query, resultsCache.nextPageNumberForQuery(query) + 1))
  133. }
  134. if (filter == query) setState(state.copy(isLoadingTail = false, dataSource = getDataSource(resultsCache.dataForQuery(query))))
  135. }
  136. }
  137. }
  138. def renderRow(movie: js.Dynamic, sectionID: String | Int, rowID: String | Int, highlightRow: js.Function2[String | Int,String | Int,_]): ReactElement = {
  139. MovieCell(movie = movie, key = movie.title.toString)
  140. }
  141. def renderFooter = {
  142. if (!hasMore || !state.isLoadingTail) View(style = styles.scrollSpinner)()
  143. else ActivityIndicatorIOS(style = styles.scrollSpinner)()
  144. }
  145. def onSearchChange(event: ReactEvent[TextInputEvent]) = {
  146. val filterLocal = event.nativeEvent.text.toLowerCase()
  147. dom.window.clearTimeout(timeoutID)
  148. timeoutID = dom.window.setTimeout(() => searchMovies(filterLocal), 100)
  149. }
  150. def dude[T <: js.Object](name: String) = "dude".asInstanceOf[T]
  151. def onSearchInputFocus(e: ReactEvent[TextInputEvent]) = {
  152. if (listViewMounted != null) listViewMounted.getScrollResponder().scrollTo(new ScrollPosition(0,0))
  153. }
  154. }
  155. def apply(key: UndefOr[String] = js.undefined, ref: js.Function1[Component, Unit] = null) =
  156. makeElementNoProps[Component](key = key, ref = ref)
  157. object styles extends UniversalStyleSheet {
  158. val container = style(
  159. flex := 1,
  160. backgroundColor := "white"
  161. )
  162. val centerText = style(
  163. alignItems.center
  164. )
  165. val noMoviesText = style(
  166. marginTop := 40,
  167. color := "#888888"
  168. )
  169. val separator = style(
  170. height := 1,
  171. backgroundColor := "#eeeeee"
  172. )
  173. val scrollSpinner = style(
  174. marginVertical := 20
  175. )
  176. }
  177. }