mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
parent
63ab4a25aa
commit
f4d81eb4ca
32 changed files with 956 additions and 685 deletions
266
.idea/codeStyles/Project.xml
generated
266
.idea/codeStyles/Project.xml
generated
|
|
@ -7,13 +7,6 @@
|
||||||
<option name="TAB_SIZE" value="2" />
|
<option name="TAB_SIZE" value="2" />
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
<AndroidXmlCodeStyleSettings>
|
|
||||||
<option name="LAYOUT_SETTINGS">
|
|
||||||
<value>
|
|
||||||
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
|
|
||||||
</value>
|
|
||||||
</option>
|
|
||||||
</AndroidXmlCodeStyleSettings>
|
|
||||||
<JSCodeStyleSettings>
|
<JSCodeStyleSettings>
|
||||||
<option name="INDENT_CHAINED_CALLS" value="false" />
|
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||||
</JSCodeStyleSettings>
|
</JSCodeStyleSettings>
|
||||||
|
|
@ -248,6 +241,28 @@
|
||||||
</match>
|
</match>
|
||||||
</rule>
|
</rule>
|
||||||
</section>
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<rule>
|
<rule>
|
||||||
<match>
|
<match>
|
||||||
|
|
@ -275,211 +290,12 @@
|
||||||
<rule>
|
<rule>
|
||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*:.*Style</NAME>
|
<NAME>.*</NAME>
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_width</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_height</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_weight</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_margin</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_marginTop</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_marginBottom</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_marginStart</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_marginEnd</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_marginLeft</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_marginRight</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:layout_.*</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:padding</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:paddingTop</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:paddingBottom</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:paddingStart</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:paddingEnd</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:paddingLeft</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*:paddingRight</NAME>
|
|
||||||
<XML_ATTRIBUTE />
|
<XML_ATTRIBUTE />
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
</rule>
|
</rule>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -487,39 +303,7 @@
|
||||||
<match>
|
<match>
|
||||||
<AND>
|
<AND>
|
||||||
<NAME>.*</NAME>
|
<NAME>.*</NAME>
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
<XML_ATTRIBUTE />
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
|
|
||||||
</AND>
|
|
||||||
</match>
|
|
||||||
<order>BY_NAME</order>
|
|
||||||
</rule>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<rule>
|
|
||||||
<match>
|
|
||||||
<AND>
|
|
||||||
<NAME>.*</NAME>
|
|
||||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
</AND>
|
</AND>
|
||||||
</match>
|
</match>
|
||||||
|
|
|
||||||
|
|
@ -43,10 +43,6 @@ dependencies {
|
||||||
implementation 'com.karumi:dexter:5.0.0'
|
implementation 'com.karumi:dexter:5.0.0'
|
||||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||||
|
|
||||||
//paging
|
|
||||||
implementation "androidx.paging:paging-runtime-ktx:2.1.2"
|
|
||||||
implementation "androidx.paging:paging-rxjava2-ktx:2.1.2"
|
|
||||||
|
|
||||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
|
||||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.View.GONE
|
||||||
|
import android.view.View.VISIBLE
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.paging.PagedListAdapter
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
|
import androidx.recyclerview.widget.MergeAdapter
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil
|
||||||
|
import kotlinx.android.synthetic.main.fragment_search_paginated.*
|
||||||
|
|
||||||
|
|
||||||
|
abstract class BaseSearchFragment<T> : CommonsDaggerSupportFragment(),
|
||||||
|
SearchFragmentContract.View<T> {
|
||||||
|
|
||||||
|
abstract val pagedListAdapter: PagedListAdapter<T,*>
|
||||||
|
abstract val injectedPresenter: SearchFragmentContract.Presenter<T>
|
||||||
|
abstract val emptyTemplateTextId: Int
|
||||||
|
private val loadingAdapter by lazy { FooterAdapter { injectedPresenter.retryFailedRequest() } }
|
||||||
|
private val mergeAdapter by lazy { MergeAdapter(pagedListAdapter, loadingAdapter) }
|
||||||
|
private var searchResults: LiveData<PagedList<T>>? = null
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
) = inflater.inflate(R.layout.fragment_search_paginated, container, false)
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
paginatedSearchResultsList.apply {
|
||||||
|
layoutManager = GridLayoutManager(context, if (isPortrait) 1 else 2)
|
||||||
|
adapter = mergeAdapter
|
||||||
|
}
|
||||||
|
injectedPresenter.listFooterData.observe(
|
||||||
|
viewLifecycleOwner,
|
||||||
|
Observer(loadingAdapter::submitList)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun observeSearchResults(searchResults: LiveData<PagedList<T>>) {
|
||||||
|
this.searchResults?.removeObservers(viewLifecycleOwner)
|
||||||
|
this.searchResults = searchResults
|
||||||
|
searchResults.observe(viewLifecycleOwner, Observer {
|
||||||
|
pagedListAdapter.submitList(it)
|
||||||
|
contentNotFound.visibility = if (it.loadedCount == 0) VISIBLE else GONE
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttach(context: Context) {
|
||||||
|
super.onAttach(context)
|
||||||
|
injectedPresenter.onAttachView(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
override fun onDetach() {
|
||||||
|
super.onDetach()
|
||||||
|
injectedPresenter.onDetachView()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setEmptyViewText(query: String) {
|
||||||
|
contentNotFound.text = getString(emptyTemplateTextId, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hideInitialLoadProgress() {
|
||||||
|
paginatedSearchInitialLoadProgress.visibility = View.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showInitialLoadInProgress() {
|
||||||
|
paginatedSearchInitialLoadProgress.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun showSnackbar() {
|
||||||
|
ViewUtil.showShortSnackbar(paginatedSearchResultsList, R.string.error_loading_depictions)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onQueryUpdated(query: String) {
|
||||||
|
injectedPresenter.onQueryUpdated(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Fragment.isPortrait get() = orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
private val Fragment.orientation get() = activity!!.resources.configuration.orientation
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import fr.free.nrw.commons.upload.depicts.proxy
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
|
||||||
|
abstract class BaseSearchPresenter<T>(
|
||||||
|
val mainThreadScheduler: Scheduler,
|
||||||
|
val pageableDataSource: PageableDataSource<T>
|
||||||
|
) : SearchFragmentContract.Presenter<T> {
|
||||||
|
|
||||||
|
private val DUMMY: SearchFragmentContract.View<T> = proxy()
|
||||||
|
private var view: SearchFragmentContract.View<T> = DUMMY
|
||||||
|
private var currentQuery: String? = null
|
||||||
|
|
||||||
|
|
||||||
|
private val compositeDisposable = CompositeDisposable()
|
||||||
|
override val listFooterData = MutableLiveData<List<FooterItem>>().apply { value = emptyList() }
|
||||||
|
|
||||||
|
override fun onAttachView(view: SearchFragmentContract.View<T>) {
|
||||||
|
this.view = view
|
||||||
|
compositeDisposable.addAll(
|
||||||
|
pageableDataSource.searchResults.subscribe(view::observeSearchResults),
|
||||||
|
pageableDataSource.loadingStates
|
||||||
|
.observeOn(mainThreadScheduler)
|
||||||
|
.subscribe(::onLoadingState, Timber::e),
|
||||||
|
pageableDataSource.noItemsLoadedEvent.subscribe {
|
||||||
|
setEmptyViewText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onLoadingState(it: LoadingState) = when (it) {
|
||||||
|
LoadingState.Loading -> listFooterData.postValue(listOf(FooterItem.LoadingItem))
|
||||||
|
LoadingState.Complete -> {
|
||||||
|
listFooterData.postValue(emptyList())
|
||||||
|
view.hideInitialLoadProgress()
|
||||||
|
}
|
||||||
|
LoadingState.InitialLoad -> view.showInitialLoadInProgress()
|
||||||
|
LoadingState.Error -> {
|
||||||
|
setEmptyViewText()
|
||||||
|
view.showSnackbar()
|
||||||
|
view.hideInitialLoadProgress()
|
||||||
|
listFooterData.postValue(listOf(FooterItem.RefreshItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEmptyViewText() {
|
||||||
|
currentQuery?.let(view::setEmptyViewText)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun retryFailedRequest() {
|
||||||
|
pageableDataSource.retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDetachView() {
|
||||||
|
view = DUMMY
|
||||||
|
compositeDisposable.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryUpdated(query: String) {
|
||||||
|
currentQuery = query
|
||||||
|
pageableDataSource.onQueryUpdated(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
|
||||||
|
abstract class BaseViewHolder<T>(override val containerView: View) :
|
||||||
|
RecyclerView.ViewHolder(containerView), LayoutContainer {
|
||||||
|
abstract fun bind(item: T)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.LayoutRes
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
|
import kotlinx.android.synthetic.main.list_item_load_more.*
|
||||||
|
|
||||||
|
class FooterAdapter(private val onRefreshClicked: () -> Unit) :
|
||||||
|
ListAdapter<FooterItem, FooterViewHolder>(object :
|
||||||
|
DiffUtil.ItemCallback<FooterItem>() {
|
||||||
|
override fun areItemsTheSame(oldItem: FooterItem, newItem: FooterItem) = oldItem == newItem
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: FooterItem, newItem: FooterItem) =
|
||||||
|
oldItem == newItem
|
||||||
|
}) {
|
||||||
|
|
||||||
|
override fun getItemViewType(position: Int): Int {
|
||||||
|
return getItem(position).ordinal
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
|
||||||
|
when (FooterItem.values()[viewType]) {
|
||||||
|
FooterItem.LoadingItem -> LoadingViewHolder(parent.inflate(R.layout.list_item_progress))
|
||||||
|
FooterItem.RefreshItem -> RefreshViewHolder(
|
||||||
|
parent.inflate(R.layout.list_item_load_more),
|
||||||
|
onRefreshClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: FooterViewHolder, position: Int) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class FooterViewHolder(override val containerView: View) :
|
||||||
|
RecyclerView.ViewHolder(containerView),
|
||||||
|
LayoutContainer
|
||||||
|
|
||||||
|
class LoadingViewHolder(containerView: View) : FooterViewHolder(containerView)
|
||||||
|
class RefreshViewHolder(containerView: View, onRefreshClicked: () -> Unit) :
|
||||||
|
FooterViewHolder(containerView) {
|
||||||
|
init {
|
||||||
|
listItemLoadMoreButton.setOnClickListener { onRefreshClicked() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class FooterItem { LoadingItem, RefreshItem }
|
||||||
|
|
||||||
|
fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = false): View =
|
||||||
|
LayoutInflater.from(context).inflate(layoutId, this, attachToRoot)
|
||||||
|
|
@ -125,7 +125,7 @@ public class SearchActivity extends NavigationBaseActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
|
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
|
||||||
searchCategoryFragment.updateCategoryList(query.toString());
|
searchCategoryFragment.onQueryUpdated(query.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||||
|
import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentContract
|
||||||
|
import fr.free.nrw.commons.explore.categories.PageableCategoriesDataSource
|
||||||
|
import io.reactivex.Scheduler
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Named
|
||||||
|
|
||||||
|
class SearchCategoriesFragmentPresenter @Inject constructor(
|
||||||
|
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||||
|
dataSourceFactory: PageableCategoriesDataSource
|
||||||
|
) : BaseSearchPresenter<String>(mainThreadScheduler, dataSourceFactory),
|
||||||
|
SearchCategoriesFragmentContract.Presenter
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import androidx.paging.PositionalDataSource
|
||||||
|
import fr.free.nrw.commons.explore.depictions.LoadFunction
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
abstract class SearchDataSource<T>(
|
||||||
|
private val loadingStates: PublishProcessor<LoadingState>
|
||||||
|
) : PositionalDataSource<T>() {
|
||||||
|
|
||||||
|
private var lastExecutedRequest: (() -> Boolean)? = null
|
||||||
|
private fun storeAndExecute(function: () -> Boolean) {
|
||||||
|
function.also { lastExecutedRequest = it }.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun performWithTryCatch(function: () -> Unit) = try {
|
||||||
|
function.invoke()
|
||||||
|
loadingStates.offer(LoadingState.Complete)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
loadingStates.offer(LoadingState.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadInitial(
|
||||||
|
params: LoadInitialParams,
|
||||||
|
callback: LoadInitialCallback<T>
|
||||||
|
) {
|
||||||
|
storeAndExecute {
|
||||||
|
loadingStates.offer(LoadingState.InitialLoad)
|
||||||
|
performWithTryCatch {
|
||||||
|
callback.onResult(
|
||||||
|
getItems(params.requestedLoadSize, params.requestedStartPosition),
|
||||||
|
params.requestedStartPosition
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
|
||||||
|
storeAndExecute {
|
||||||
|
loadingStates.offer(LoadingState.Loading)
|
||||||
|
performWithTryCatch {
|
||||||
|
callback.onResult(getItems(params.loadSize, params.startPosition))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun getItems(loadSize: Int, startPosition: Int): List<T>
|
||||||
|
|
||||||
|
fun retryFailedRequest() {
|
||||||
|
Completable.fromAction { lastExecutedRequest?.invoke() }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> dataSource(
|
||||||
|
loadingStates: PublishProcessor<LoadingState>,
|
||||||
|
loadFunction: LoadFunction<T>
|
||||||
|
) = object : SearchDataSource<T>(loadingStates) {
|
||||||
|
override fun getItems(loadSize: Int, startPosition: Int): List<T> {
|
||||||
|
return loadFunction(loadSize, startPosition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.Config
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import androidx.paging.toLiveData
|
||||||
|
import fr.free.nrw.commons.explore.depictions.LoadFunction
|
||||||
|
import fr.free.nrw.commons.explore.depictions.LoadingStates
|
||||||
|
import io.reactivex.Flowable
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 50
|
||||||
|
private const val INITIAL_LOAD_SIZE = 50
|
||||||
|
|
||||||
|
abstract class PageableDataSource<T>(private val liveDataConverter: LiveDataConverter) {
|
||||||
|
|
||||||
|
lateinit var query: String
|
||||||
|
private val dataSourceFactoryFactory: () -> SearchDataSourceFactory<T> = {
|
||||||
|
dataSourceFactory(_loadingStates, loadFunction)
|
||||||
|
}
|
||||||
|
private val _loadingStates = PublishProcessor.create<LoadingState>()
|
||||||
|
val loadingStates: Flowable<LoadingState> = _loadingStates
|
||||||
|
private val _searchResults = PublishProcessor.create<LiveData<PagedList<T>>>()
|
||||||
|
val searchResults: Flowable<LiveData<PagedList<T>>> = _searchResults
|
||||||
|
private val _noItemsLoadedEvent = PublishProcessor.create<Unit>()
|
||||||
|
val noItemsLoadedEvent: Flowable<Unit> = _noItemsLoadedEvent
|
||||||
|
private var currentFactory: SearchDataSourceFactory<T>? = null
|
||||||
|
|
||||||
|
abstract val loadFunction: LoadFunction<T>
|
||||||
|
|
||||||
|
fun onQueryUpdated(query: String) {
|
||||||
|
this.query = query
|
||||||
|
_searchResults.offer(
|
||||||
|
liveDataConverter.convert(dataSourceFactoryFactory().also { currentFactory = it }) {
|
||||||
|
_noItemsLoadedEvent.offer(Unit)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun retryFailedRequest() {
|
||||||
|
currentFactory?.retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LiveDataConverter @Inject constructor() {
|
||||||
|
fun <T> convert(
|
||||||
|
dataSourceFactory: SearchDataSourceFactory<T>,
|
||||||
|
zeroItemsLoadedFunction: () -> Unit
|
||||||
|
): LiveData<PagedList<T>> {
|
||||||
|
return dataSourceFactory.toLiveData(
|
||||||
|
Config(
|
||||||
|
pageSize = PAGE_SIZE,
|
||||||
|
initialLoadSizeHint = INITIAL_LOAD_SIZE,
|
||||||
|
enablePlaceholders = false
|
||||||
|
),
|
||||||
|
boundaryCallback = object : PagedList.BoundaryCallback<T>() {
|
||||||
|
override fun onZeroItemsLoaded() {
|
||||||
|
zeroItemsLoadedFunction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SearchDataSourceFactory<T>(val loadingStates: LoadingStates) :
|
||||||
|
DataSource.Factory<Int, T>() {
|
||||||
|
private var currentDataSource: SearchDataSource<T>? = null
|
||||||
|
abstract val loadFunction: LoadFunction<T>
|
||||||
|
|
||||||
|
override fun create() =
|
||||||
|
dataSource(loadingStates, loadFunction).also { currentDataSource = it }
|
||||||
|
|
||||||
|
fun retryFailedRequest() {
|
||||||
|
currentDataSource?.retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> dataSourceFactory(loadingStates: LoadingStates, loadFunction: LoadFunction<T>) =
|
||||||
|
object : SearchDataSourceFactory<T>(loadingStates) {
|
||||||
|
override val loadFunction: LoadFunction<T> = loadFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class LoadingState {
|
||||||
|
object InitialLoad : LoadingState()
|
||||||
|
object Loading : LoadingState()
|
||||||
|
object Complete : LoadingState()
|
||||||
|
object Error : LoadingState()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import fr.free.nrw.commons.BasePresenter
|
||||||
|
|
||||||
|
interface SearchFragmentContract {
|
||||||
|
interface View<T> {
|
||||||
|
fun showSnackbar()
|
||||||
|
fun observeSearchResults(searchResults: LiveData<PagedList<T>>)
|
||||||
|
fun setEmptyViewText(query: String)
|
||||||
|
fun showInitialLoadInProgress()
|
||||||
|
fun hideInitialLoadProgress()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Presenter<T> : BasePresenter<View<T>> {
|
||||||
|
val listFooterData: LiveData<List<FooterItem>>
|
||||||
|
fun onQueryUpdated(query: String)
|
||||||
|
fun retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore;
|
|
||||||
|
|
||||||
import dagger.Binds;
|
|
||||||
import dagger.Module;
|
|
||||||
import dagger.Provides;
|
|
||||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
|
||||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsDataSourceFactory;
|
|
||||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsDataSourceFactoryFactory;
|
|
||||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
|
|
||||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
|
|
||||||
*/
|
|
||||||
@Module
|
|
||||||
public abstract class SearchModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
public abstract SearchDepictionsFragmentContract.UserActionListener bindsSearchDepictionsFragmentPresenter(
|
|
||||||
SearchDepictionsFragmentPresenter
|
|
||||||
presenter
|
|
||||||
);
|
|
||||||
|
|
||||||
@Provides
|
|
||||||
static public SearchDepictionsDataSourceFactoryFactory providesSearchDepictionsFactoryFactory(
|
|
||||||
DepictsClient depictsClient){
|
|
||||||
return (query, loadingStates) -> new SearchDepictionsDataSourceFactory(depictsClient, query, loadingStates);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import fr.free.nrw.commons.explore.categories.SearchCategoriesFragmentContract
|
||||||
|
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract
|
||||||
|
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
|
||||||
|
*/
|
||||||
|
@Module
|
||||||
|
abstract class SearchModule {
|
||||||
|
@Binds
|
||||||
|
abstract fun bindsSearchDepictionsFragmentPresenter(
|
||||||
|
presenter: SearchDepictionsFragmentPresenter
|
||||||
|
): SearchDepictionsFragmentContract.Presenter
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindsSearchCategoriesFragmentPresenter(
|
||||||
|
presenter: SearchCategoriesFragmentPresenter?
|
||||||
|
): SearchCategoriesFragmentContract.Presenter?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package fr.free.nrw.commons.explore.categories
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.category.CategoryClient
|
||||||
|
import fr.free.nrw.commons.explore.LiveDataConverter
|
||||||
|
import fr.free.nrw.commons.explore.PageableDataSource
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class PageableCategoriesDataSource @Inject constructor(
|
||||||
|
liveDataConverter: LiveDataConverter,
|
||||||
|
val categoryClient: CategoryClient
|
||||||
|
) : PageableDataSource<String>(liveDataConverter) {
|
||||||
|
|
||||||
|
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||||
|
categoryClient.searchCategories(query, loadSize, startPosition).blockingFirst()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
package fr.free.nrw.commons.explore.categories
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.PagedListAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.category.CATEGORY_PREFIX
|
||||||
|
import fr.free.nrw.commons.explore.BaseViewHolder
|
||||||
|
import fr.free.nrw.commons.explore.inflate
|
||||||
|
import kotlinx.android.synthetic.main.item_recent_searches.*
|
||||||
|
|
||||||
|
|
||||||
|
class PagedSearchCategoriesAdapter(val onCategoryClicked: (String) -> Unit) :
|
||||||
|
PagedListAdapter<String, CategoryItemViewHolder>(
|
||||||
|
object : DiffUtil.ItemCallback<String>() {
|
||||||
|
override fun areItemsTheSame(oldItem: String, newItem: String) =
|
||||||
|
oldItem == newItem
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: String, newItem: String) =
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CategoryItemViewHolder {
|
||||||
|
return CategoryItemViewHolder(
|
||||||
|
parent.inflate(R.layout.item_recent_searches),
|
||||||
|
onCategoryClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: CategoryItemViewHolder, position: Int) {
|
||||||
|
holder.bind(getItem(position)!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CategoryItemViewHolder(containerView: View, val onCategoryClicked: (String) -> Unit) :
|
||||||
|
BaseViewHolder<String>(containerView) {
|
||||||
|
|
||||||
|
override fun bind(item: String) {
|
||||||
|
containerView.setOnClickListener { onCategoryClicked(item) }
|
||||||
|
textView1.text = item.substringAfter(CATEGORY_PREFIX)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package fr.free.nrw.commons.explore.categories
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.explore.SearchFragmentContract
|
||||||
|
|
||||||
|
interface SearchCategoriesFragmentContract {
|
||||||
|
interface View : SearchFragmentContract.View<String>
|
||||||
|
interface Presenter : SearchFragmentContract.Presenter<String>
|
||||||
|
}
|
||||||
|
|
@ -1,214 +0,0 @@
|
||||||
package fr.free.nrw.commons.explore.categories;
|
|
||||||
|
|
||||||
|
|
||||||
import static android.view.View.GONE;
|
|
||||||
import static android.view.View.VISIBLE;
|
|
||||||
|
|
||||||
import android.content.res.Configuration;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.category.CategoryClient;
|
|
||||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
|
|
||||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
|
||||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|
||||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
import kotlin.Unit;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the category search screen.
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.imagesListBox)
|
|
||||||
RecyclerView categoriesRecyclerView;
|
|
||||||
@BindView(R.id.imageSearchInProgress)
|
|
||||||
ProgressBar progressBar;
|
|
||||||
@BindView(R.id.imagesNotFound)
|
|
||||||
TextView categoriesNotFoundView;
|
|
||||||
String query;
|
|
||||||
@BindView(R.id.bottomProgressBar)
|
|
||||||
ProgressBar bottomProgressBar;
|
|
||||||
boolean isLoadingCategories;
|
|
||||||
|
|
||||||
@Inject RecentSearchesDao recentSearchesDao;
|
|
||||||
@Inject CategoryClient categoryClient;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
JsonKvStore basicKvStore;
|
|
||||||
|
|
||||||
private SearchCategoriesAdapter categoriesAdapter;
|
|
||||||
private List<String> queryList = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method saves Search Query in the Recent Searches Database.
|
|
||||||
* @param query
|
|
||||||
*/
|
|
||||||
private void saveQuery(String query) {
|
|
||||||
RecentSearch recentSearch = recentSearchesDao.find(query);
|
|
||||||
|
|
||||||
// Newly searched query...
|
|
||||||
if (recentSearch == null) {
|
|
||||||
recentSearch = new RecentSearch(null, query, new Date());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
recentSearch.setLastSearched(new Date());
|
|
||||||
}
|
|
||||||
recentSearchesDao.save(recentSearch);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
|
|
||||||
ButterKnife.bind(this, rootView);
|
|
||||||
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
|
|
||||||
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
|
|
||||||
}
|
|
||||||
categoriesAdapter = new SearchCategoriesAdapter(item -> {
|
|
||||||
CategoryDetailsActivity.startYourself(getContext(), item);
|
|
||||||
saveQuery(query);
|
|
||||||
return Unit.INSTANCE;
|
|
||||||
});
|
|
||||||
categoriesRecyclerView.setAdapter(categoriesAdapter);
|
|
||||||
categoriesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
|
||||||
@Override
|
|
||||||
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
|
|
||||||
super.onScrollStateChanged(recyclerView, newState);
|
|
||||||
// check if end of recycler view is reached, if yes then add more results to existing results
|
|
||||||
if (!recyclerView.canScrollVertically(1)) {
|
|
||||||
addCategoriesToList(query);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks for internet connection and then initializes the recycler view with 25 categories of the searched query
|
|
||||||
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
|
|
||||||
*/
|
|
||||||
public void updateCategoryList(String query) {
|
|
||||||
this.query = query;
|
|
||||||
categoriesNotFoundView.setVisibility(GONE);
|
|
||||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
|
||||||
handleNoInternet();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
bottomProgressBar.setVisibility(View.VISIBLE);
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
queryList.clear();
|
|
||||||
categoriesAdapter.clear();
|
|
||||||
compositeDisposable.add(categoryClient.searchCategories(query,25)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnSubscribe(disposable -> saveQuery(query))
|
|
||||||
.subscribe(this::handleSuccess, this::handleError));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds 25 more results to existing search results
|
|
||||||
*/
|
|
||||||
public void addCategoriesToList(String query) {
|
|
||||||
if(isLoadingCategories) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
isLoadingCategories=true;
|
|
||||||
this.query = query;
|
|
||||||
bottomProgressBar.setVisibility(View.VISIBLE);
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
compositeDisposable.add(categoryClient.searchCategories(query,25, queryList.size())
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(this::handlePaginationSuccess, this::handleError));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the success scenario
|
|
||||||
* it initializes the recycler view by adding items to the adapter
|
|
||||||
*/
|
|
||||||
private void handlePaginationSuccess(List<String> mediaList) {
|
|
||||||
queryList.addAll(mediaList);
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
bottomProgressBar.setVisibility(GONE);
|
|
||||||
categoriesAdapter.addAll(mediaList);
|
|
||||||
isLoadingCategories=false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the success scenario
|
|
||||||
* it initializes the recycler view by adding items to the adapter
|
|
||||||
*/
|
|
||||||
private void handleSuccess(List<String> mediaList) {
|
|
||||||
queryList = mediaList;
|
|
||||||
if (mediaList == null || mediaList.isEmpty()) {
|
|
||||||
initErrorView();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
bottomProgressBar.setVisibility(View.GONE);
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
categoriesAdapter.addAll(mediaList);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Logs and handles API error scenario
|
|
||||||
*/
|
|
||||||
private void handleError(Throwable throwable) {
|
|
||||||
Timber.e(throwable, "Error occurred while loading queried categories");
|
|
||||||
try {
|
|
||||||
initErrorView();
|
|
||||||
ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.error_loading_categories);
|
|
||||||
}catch (Exception e){
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for a error scenario
|
|
||||||
*/
|
|
||||||
private void initErrorView() {
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
categoriesNotFoundView.setVisibility(VISIBLE);
|
|
||||||
categoriesNotFoundView.setText(getString(R.string.categories_not_found,query));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the UI updates for no internet scenario
|
|
||||||
*/
|
|
||||||
private void handleNoInternet() {
|
|
||||||
progressBar.setVisibility(GONE);
|
|
||||||
ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.no_internet);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
package fr.free.nrw.commons.explore.categories
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.category.CategoryDetailsActivity
|
||||||
|
import fr.free.nrw.commons.explore.BaseSearchFragment
|
||||||
|
import fr.free.nrw.commons.explore.SearchFragmentContract
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the category search screen.
|
||||||
|
*/
|
||||||
|
class SearchCategoryFragment : BaseSearchFragment<String>() {
|
||||||
|
@Inject
|
||||||
|
lateinit var presenter: SearchCategoriesFragmentContract.Presenter
|
||||||
|
|
||||||
|
override val emptyTemplateTextId: Int = R.string.categories_not_found
|
||||||
|
|
||||||
|
override val injectedPresenter: SearchFragmentContract.Presenter<String>
|
||||||
|
get() = presenter
|
||||||
|
|
||||||
|
override val pagedListAdapter by lazy {
|
||||||
|
PagedSearchCategoriesAdapter { CategoryDetailsActivity.startYourself(context, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import androidx.paging.PagedListAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.explore.inflate
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import kotlinx.android.extensions.LayoutContainer
|
import kotlinx.android.extensions.LayoutContainer
|
||||||
import kotlinx.android.synthetic.main.item_depictions.*
|
import kotlinx.android.synthetic.main.item_depictions.*
|
||||||
|
|
@ -19,9 +20,7 @@ class DepictionAdapter(val onDepictionClicked: (DepictedItem) -> Unit) :
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) =
|
||||||
oldItem == newItem
|
oldItem == newItem
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
) {
|
) {
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DepictedItemViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DepictedItemViewHolder {
|
||||||
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions))
|
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions))
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.explore.LiveDataConverter
|
||||||
|
import fr.free.nrw.commons.explore.LoadingState
|
||||||
|
import fr.free.nrw.commons.explore.PageableDataSource
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
typealias LoadFunction<T> = (Int, Int) -> List<T>
|
||||||
|
typealias LoadingStates = PublishProcessor<LoadingState>
|
||||||
|
|
||||||
|
class PageableDepictionsDataSource @Inject constructor(
|
||||||
|
liveDataConverter: LiveDataConverter,
|
||||||
|
val depictsClient: DepictsClient
|
||||||
|
) : PageableDataSource<DepictedItem>(liveDataConverter) {
|
||||||
|
|
||||||
|
override val loadFunction = { loadSize: Int, startPosition: Int ->
|
||||||
|
depictsClient.searchForDepictions(query, loadSize, startPosition).blockingGet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -1,99 +1,26 @@
|
||||||
package fr.free.nrw.commons.explore.depictions
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.GONE
|
|
||||||
import android.view.View.VISIBLE
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.Observer
|
|
||||||
import androidx.paging.PagedList
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.MergeAdapter
|
|
||||||
import fr.free.nrw.commons.R
|
import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
|
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
import fr.free.nrw.commons.explore.BaseSearchFragment
|
||||||
|
import fr.free.nrw.commons.explore.SearchFragmentContract
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import fr.free.nrw.commons.utils.ViewUtil
|
|
||||||
import kotlinx.android.synthetic.main.fragment_search_depictions.*
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display depictions in search fragment
|
* Display depictions in search fragment
|
||||||
*/
|
*/
|
||||||
class SearchDepictionsFragment : CommonsDaggerSupportFragment(),
|
class SearchDepictionsFragment : BaseSearchFragment<DepictedItem>(),
|
||||||
SearchDepictionsFragmentContract.View {
|
SearchDepictionsFragmentContract.View {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var presenter: SearchDepictionsFragmentContract.UserActionListener
|
lateinit var presenter: SearchDepictionsFragmentContract.Presenter
|
||||||
|
|
||||||
private val depictionsAdapter by lazy {
|
override val emptyTemplateTextId: Int = R.string.depictions_not_found
|
||||||
|
|
||||||
|
override val injectedPresenter: SearchFragmentContract.Presenter<DepictedItem>
|
||||||
|
get() = presenter
|
||||||
|
|
||||||
|
override val pagedListAdapter by lazy {
|
||||||
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
|
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
|
||||||
}
|
}
|
||||||
private val loadingAdapter by lazy { FooterAdapter { presenter.retryFailedRequest() } }
|
|
||||||
private val mergeAdapter by lazy { MergeAdapter(depictionsAdapter, loadingAdapter) }
|
|
||||||
|
|
||||||
var searchResults: LiveData<PagedList<DepictedItem>>? = null
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
) = inflater.inflate(R.layout.fragment_search_depictions, container, false)
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
depictionsSearchResultsList.apply {
|
|
||||||
layoutManager = GridLayoutManager(context, if (isPortrait) 1 else 2)
|
|
||||||
adapter = mergeAdapter
|
|
||||||
}
|
|
||||||
presenter.listFooterData.observe(viewLifecycleOwner, Observer(loadingAdapter::submitList))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttach(context: Context) {
|
|
||||||
super.onAttach(context)
|
|
||||||
presenter.onAttachView(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach() {
|
|
||||||
super.onDetach()
|
|
||||||
presenter.onDetachView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeSearchResults(searchResults: LiveData<PagedList<DepictedItem>>) {
|
|
||||||
this.searchResults?.removeObservers(viewLifecycleOwner)
|
|
||||||
this.searchResults = searchResults
|
|
||||||
searchResults.observe(viewLifecycleOwner, Observer {
|
|
||||||
depictionsAdapter.submitList(it)
|
|
||||||
depictionNotFound.visibility = if (it.loadedCount == 0) VISIBLE else GONE
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setEmptyViewText(query: String) {
|
|
||||||
depictionNotFound.text = getString(R.string.depictions_not_found, query)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun hideInitialLoadProgress() {
|
|
||||||
depictionSearchInitialLoadProgress.visibility = GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showInitialLoadInProgress() {
|
|
||||||
depictionSearchInitialLoadProgress.visibility = VISIBLE
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun showSnackbar() {
|
|
||||||
ViewUtil.showShortSnackbar(depictionsSearchResultsList, R.string.error_loading_depictions)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onQueryUpdated(query: String) {
|
|
||||||
presenter.onQueryUpdated(query)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val Fragment.isPortrait get() = orientation == Configuration.ORIENTATION_PORTRAIT
|
|
||||||
|
|
||||||
private val Fragment.orientation get() = activity!!.resources.configuration.orientation
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,12 @@
|
||||||
package fr.free.nrw.commons.explore.depictions
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
import androidx.lifecycle.LiveData
|
import fr.free.nrw.commons.explore.SearchFragmentContract
|
||||||
import androidx.paging.PagedList
|
|
||||||
import fr.free.nrw.commons.BasePresenter
|
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
|
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
|
||||||
*/
|
*/
|
||||||
interface SearchDepictionsFragmentContract {
|
interface SearchDepictionsFragmentContract {
|
||||||
interface View {
|
interface View : SearchFragmentContract.View<DepictedItem>
|
||||||
fun showSnackbar()
|
interface Presenter : SearchFragmentContract.Presenter<DepictedItem>
|
||||||
fun observeSearchResults(searchResults: LiveData<PagedList<DepictedItem>>)
|
|
||||||
fun setEmptyViewText(query: String)
|
|
||||||
fun showInitialLoadInProgress()
|
|
||||||
fun hideInitialLoadProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UserActionListener : BasePresenter<View?> {
|
|
||||||
val listFooterData: LiveData<List<FooterItem>>
|
|
||||||
fun onQueryUpdated(query: String)
|
|
||||||
fun retryFailedRequest()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
package fr.free.nrw.commons.explore.depictions
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import fr.free.nrw.commons.di.CommonsApplicationModule
|
import fr.free.nrw.commons.di.CommonsApplicationModule
|
||||||
import fr.free.nrw.commons.upload.depicts.proxy
|
import fr.free.nrw.commons.explore.BaseSearchPresenter
|
||||||
|
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||||
import io.reactivex.Scheduler
|
import io.reactivex.Scheduler
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Named
|
import javax.inject.Named
|
||||||
|
|
||||||
|
|
@ -13,61 +11,7 @@ import javax.inject.Named
|
||||||
* The presenter class for SearchDepictionsFragment
|
* The presenter class for SearchDepictionsFragment
|
||||||
*/
|
*/
|
||||||
class SearchDepictionsFragmentPresenter @Inject constructor(
|
class SearchDepictionsFragmentPresenter @Inject constructor(
|
||||||
@param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler,
|
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
|
||||||
private val searchableDataSourceFactory: SearchableDepictionsDataSourceFactory
|
dataSourceFactory: PageableDepictionsDataSource
|
||||||
) : SearchDepictionsFragmentContract.UserActionListener {
|
) : BaseSearchPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
|
||||||
private val compositeDisposable = CompositeDisposable()
|
SearchDepictionsFragmentContract.Presenter
|
||||||
private var view = DUMMY
|
|
||||||
private var currentQuery: String? = null
|
|
||||||
override val listFooterData = MutableLiveData<List<FooterItem>>().apply { value = emptyList() }
|
|
||||||
|
|
||||||
override fun onAttachView(view: SearchDepictionsFragmentContract.View) {
|
|
||||||
this.view = view
|
|
||||||
compositeDisposable.addAll(
|
|
||||||
searchableDataSourceFactory.searchResults.subscribe(view::observeSearchResults),
|
|
||||||
searchableDataSourceFactory.loadingStates
|
|
||||||
.observeOn(mainThreadScheduler)
|
|
||||||
.subscribe(::onLoadingState, Timber::e),
|
|
||||||
searchableDataSourceFactory.noItemsLoadedEvent.subscribe {
|
|
||||||
setEmptyViewText()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onLoadingState(it: LoadingState) = when (it) {
|
|
||||||
LoadingState.Loading -> listFooterData.postValue(listOf(FooterItem.LoadingItem))
|
|
||||||
LoadingState.Complete -> {
|
|
||||||
listFooterData.postValue(emptyList())
|
|
||||||
view.hideInitialLoadProgress()
|
|
||||||
}
|
|
||||||
LoadingState.InitialLoad -> view.showInitialLoadInProgress()
|
|
||||||
LoadingState.Error -> {
|
|
||||||
setEmptyViewText()
|
|
||||||
view.showSnackbar()
|
|
||||||
view.hideInitialLoadProgress()
|
|
||||||
listFooterData.postValue(listOf(FooterItem.RefreshItem))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setEmptyViewText() {
|
|
||||||
currentQuery?.let(view::setEmptyViewText)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun retryFailedRequest() {
|
|
||||||
searchableDataSourceFactory.retryFailedRequest()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetachView() {
|
|
||||||
view = DUMMY
|
|
||||||
compositeDisposable.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryUpdated(query: String) {
|
|
||||||
currentQuery = query
|
|
||||||
searchableDataSourceFactory.onQueryUpdated(query)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val DUMMY: SearchDepictionsFragmentContract.View = proxy()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import fr.free.nrw.commons.MediaDataExtractor;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.Utils;
|
import fr.free.nrw.commons.Utils;
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
|
import fr.free.nrw.commons.category.CategoryClient;
|
||||||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
||||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ import android.widget.RelativeLayout;
|
||||||
import android.widget.SearchView;
|
import android.widget.SearchView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
|
||||||
29
app/src/main/res/layout/fragment_search_paginated.xml
Normal file
29
app/src/main/res/layout/fragment_search_paginated.xml
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingTop="@dimen/tiny_gap">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/contentNotFound"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:gravity="center"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/paginatedSearchResultsList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scrollbarSize="@dimen/standard_gap"
|
||||||
|
android:scrollbars="vertical" />
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/paginatedSearchInitialLoadProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package fr.free.nrw.commons.explore.depictions
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.paging.PagedList
|
import androidx.paging.PagedList
|
||||||
import com.jraska.livedata.test
|
import com.jraska.livedata.test
|
||||||
import com.nhaarman.mockitokotlin2.*
|
import com.nhaarman.mockitokotlin2.*
|
||||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
|
||||||
import io.reactivex.processors.PublishProcessor
|
import io.reactivex.processors.PublishProcessor
|
||||||
import io.reactivex.schedulers.TestScheduler
|
import io.reactivex.schedulers.TestScheduler
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
|
@ -14,25 +13,25 @@ import org.junit.Test
|
||||||
import org.mockito.Mock
|
import org.mockito.Mock
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
class SearchDepictionsFragmentPresenterTest {
|
class BaseSearchPresenterTest {
|
||||||
|
|
||||||
@Rule
|
@Rule
|
||||||
@JvmField
|
@JvmField
|
||||||
var instantTaskExecutorRule = InstantTaskExecutorRule()
|
var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
internal lateinit var view: SearchDepictionsFragmentContract.View
|
internal lateinit var view: SearchFragmentContract.View<String>
|
||||||
|
|
||||||
private lateinit var searchDepictionsFragmentPresenter: SearchDepictionsFragmentPresenter
|
private lateinit var baseSearchPresenter: BaseSearchPresenter<String>
|
||||||
|
|
||||||
private lateinit var testScheduler: TestScheduler
|
private lateinit var testScheduler: TestScheduler
|
||||||
|
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var searchableDepictionsDataSourceFactory: SearchableDepictionsDataSourceFactory
|
private lateinit var pageableDataSource: PageableDataSource<String>
|
||||||
|
|
||||||
private var loadingStates: PublishProcessor<LoadingState> = PublishProcessor.create()
|
private var loadingStates: PublishProcessor<LoadingState> = PublishProcessor.create()
|
||||||
|
|
||||||
private var searchResults: PublishProcessor<LiveData<PagedList<DepictedItem>>> =
|
private var searchResults: PublishProcessor<LiveData<PagedList<String>>> =
|
||||||
PublishProcessor.create()
|
PublishProcessor.create()
|
||||||
|
|
||||||
private var noItemLoadedEvent: PublishProcessor<Unit> = PublishProcessor.create()
|
private var noItemLoadedEvent: PublishProcessor<Unit> = PublishProcessor.create()
|
||||||
|
|
@ -41,21 +40,19 @@ class SearchDepictionsFragmentPresenterTest {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
whenever(searchableDepictionsDataSourceFactory.searchResults).thenReturn(searchResults)
|
whenever(pageableDataSource.searchResults).thenReturn(searchResults)
|
||||||
whenever(searchableDepictionsDataSourceFactory.loadingStates).thenReturn(loadingStates)
|
whenever(pageableDataSource.loadingStates).thenReturn(loadingStates)
|
||||||
whenever(searchableDepictionsDataSourceFactory.noItemsLoadedEvent)
|
whenever(pageableDataSource.noItemsLoadedEvent)
|
||||||
.thenReturn(noItemLoadedEvent)
|
.thenReturn(noItemLoadedEvent)
|
||||||
testScheduler = TestScheduler()
|
testScheduler = TestScheduler()
|
||||||
searchDepictionsFragmentPresenter = SearchDepictionsFragmentPresenter(
|
baseSearchPresenter =
|
||||||
testScheduler,
|
object : BaseSearchPresenter<String>(testScheduler, pageableDataSource) {}
|
||||||
searchableDepictionsDataSourceFactory
|
baseSearchPresenter.onAttachView(view)
|
||||||
)
|
|
||||||
searchDepictionsFragmentPresenter.onAttachView(view)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `searchResults emission updates the view`() {
|
fun `searchResults emission updates the view`() {
|
||||||
val pagedListLiveData = mock<LiveData<PagedList<DepictedItem>>>()
|
val pagedListLiveData = mock<LiveData<PagedList<String>>>()
|
||||||
searchResults.offer(pagedListLiveData)
|
searchResults.offer(pagedListLiveData)
|
||||||
verify(view).observeSearchResults(pagedListLiveData)
|
verify(view).observeSearchResults(pagedListLiveData)
|
||||||
}
|
}
|
||||||
|
|
@ -63,14 +60,13 @@ class SearchDepictionsFragmentPresenterTest {
|
||||||
@Test
|
@Test
|
||||||
fun `Loading offers a loading list item`() {
|
fun `Loading offers a loading list item`() {
|
||||||
onLoadingState(LoadingState.Loading)
|
onLoadingState(LoadingState.Loading)
|
||||||
searchDepictionsFragmentPresenter.listFooterData.test()
|
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.LoadingItem))
|
||||||
.assertValue(listOf(FooterItem.LoadingItem))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Complete offers an empty list item and hides initial loader`() {
|
fun `Complete offers an empty list item and hides initial loader`() {
|
||||||
onLoadingState(LoadingState.Complete)
|
onLoadingState(LoadingState.Complete)
|
||||||
searchDepictionsFragmentPresenter.listFooterData.test()
|
baseSearchPresenter.listFooterData.test()
|
||||||
.assertValue(emptyList())
|
.assertValue(emptyList())
|
||||||
verify(view).hideInitialLoadProgress()
|
verify(view).hideInitialLoadProgress()
|
||||||
}
|
}
|
||||||
|
|
@ -83,13 +79,12 @@ class SearchDepictionsFragmentPresenterTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() {
|
fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() {
|
||||||
searchDepictionsFragmentPresenter.onQueryUpdated("test")
|
baseSearchPresenter.onQueryUpdated("test")
|
||||||
onLoadingState(LoadingState.Error)
|
onLoadingState(LoadingState.Error)
|
||||||
verify(view).setEmptyViewText("test")
|
verify(view).setEmptyViewText("test")
|
||||||
verify(view).showSnackbar()
|
verify(view).showSnackbar()
|
||||||
verify(view).hideInitialLoadProgress()
|
verify(view).hideInitialLoadProgress()
|
||||||
searchDepictionsFragmentPresenter.listFooterData.test()
|
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem))
|
||||||
.assertValue(listOf(FooterItem.RefreshItem))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -98,35 +93,33 @@ class SearchDepictionsFragmentPresenterTest {
|
||||||
verify(view, never()).setEmptyViewText(any())
|
verify(view, never()).setEmptyViewText(any())
|
||||||
verify(view).showSnackbar()
|
verify(view).showSnackbar()
|
||||||
verify(view).hideInitialLoadProgress()
|
verify(view).hideInitialLoadProgress()
|
||||||
searchDepictionsFragmentPresenter.listFooterData.test()
|
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem))
|
||||||
.assertValue(listOf(FooterItem.RefreshItem))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `no Items event sets empty view text`() {
|
fun `no Items event sets empty view text`() {
|
||||||
searchDepictionsFragmentPresenter.onQueryUpdated("test")
|
baseSearchPresenter.onQueryUpdated("test")
|
||||||
noItemLoadedEvent.offer(Unit)
|
noItemLoadedEvent.offer(Unit)
|
||||||
verify(view).setEmptyViewText("test")
|
verify(view).setEmptyViewText("test")
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `retryFailedRequest calls retry`() {
|
fun `retryFailedRequest calls retry`() {
|
||||||
searchDepictionsFragmentPresenter.retryFailedRequest()
|
baseSearchPresenter.retryFailedRequest()
|
||||||
verify(searchableDepictionsDataSourceFactory).retryFailedRequest()
|
verify(pageableDataSource).retryFailedRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onDetachView stops subscriptions`() {
|
fun `onDetachView stops subscriptions`() {
|
||||||
searchDepictionsFragmentPresenter.onDetachView()
|
baseSearchPresenter.onDetachView()
|
||||||
onLoadingState(LoadingState.Loading)
|
onLoadingState(LoadingState.Loading)
|
||||||
searchDepictionsFragmentPresenter.listFooterData.test()
|
baseSearchPresenter.listFooterData.test().assertValue(emptyList())
|
||||||
.assertValue(emptyList())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `onQueryUpdated updates dataSourceFactory`() {
|
fun `onQueryUpdated updates dataSourceFactory`() {
|
||||||
searchDepictionsFragmentPresenter.onQueryUpdated("test")
|
baseSearchPresenter.onQueryUpdated("test")
|
||||||
verify(searchableDepictionsDataSourceFactory).onQueryUpdated("test")
|
verify(pageableDataSource).onQueryUpdated("test")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoadingState(loadingState: LoadingState) {
|
private fun onLoadingState(loadingState: LoadingState) {
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.paging.PagedList
|
||||||
|
import com.nhaarman.mockitokotlin2.*
|
||||||
|
import fr.free.nrw.commons.explore.depictions.LoadFunction
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class PageableDataSourceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var liveDataConverter: LiveDataConverter
|
||||||
|
|
||||||
|
private lateinit var pageableDataSource: PageableDataSource<String>
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
pageableDataSource = object: PageableDataSource<String>(liveDataConverter){
|
||||||
|
override val loadFunction: LoadFunction<String>
|
||||||
|
get() = mock()
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onQueryUpdated emits new liveData`() {
|
||||||
|
val (_, liveData) = expectNewLiveData()
|
||||||
|
pageableDataSource.searchResults.test()
|
||||||
|
.also { pageableDataSource.onQueryUpdated("test") }
|
||||||
|
.assertValue(liveData)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `onQueryUpdated invokes livedatconverter with no items emitter`() {
|
||||||
|
val (zeroItemsFuncCaptor, _) = expectNewLiveData()
|
||||||
|
pageableDataSource.onQueryUpdated("test")
|
||||||
|
pageableDataSource.noItemsLoadedEvent.test()
|
||||||
|
.also { zeroItemsFuncCaptor.firstValue.invoke() }
|
||||||
|
.assertValue(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Just for coverage, no way to really assert this
|
||||||
|
* */
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest does nothing without a factory`() {
|
||||||
|
pageableDataSource.retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore("Rewrite with Mockk constructor mocks")
|
||||||
|
fun `retryFailedRequest retries with a factory`() {
|
||||||
|
val (_, _, dataSourceFactoryCaptor) = expectNewLiveData()
|
||||||
|
pageableDataSource.onQueryUpdated("test")
|
||||||
|
val dataSourceFactory = spy(dataSourceFactoryCaptor.firstValue)
|
||||||
|
pageableDataSource.retryFailedRequest()
|
||||||
|
verify(dataSourceFactory).retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<String>>, KArgumentCaptor<SearchDataSourceFactory<String>>> {
|
||||||
|
val captor = argumentCaptor<() -> Unit>()
|
||||||
|
val dataSourceFactoryCaptor = argumentCaptor<SearchDataSourceFactory<String>>()
|
||||||
|
val liveData: LiveData<PagedList<String>> = mock()
|
||||||
|
whenever(liveDataConverter.convert(dataSourceFactoryCaptor.capture(), captor.capture()))
|
||||||
|
.thenReturn(liveData)
|
||||||
|
return Triple(captor, liveData, dataSourceFactoryCaptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.spy
|
||||||
|
import com.nhaarman.mockitokotlin2.verify
|
||||||
|
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers.instanceOf
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Ignore
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class SearchDataSourceFactoryTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var depictsClient: DepictsClient
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var loadingStates: PublishProcessor<LoadingState>
|
||||||
|
private lateinit var factory: SearchDataSourceFactory<String>
|
||||||
|
|
||||||
|
private var function: (Int, Int) -> List<String> = mock()
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
factory = object : SearchDataSourceFactory<String>(loadingStates) {
|
||||||
|
override val loadFunction get() = function
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `create returns a dataSource`() {
|
||||||
|
assertThat(
|
||||||
|
factory.create(),
|
||||||
|
instanceOf(SearchDataSource::class.java)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Ignore("Rewrite with Mockk constructor mocks")
|
||||||
|
fun `retryFailedRequest invokes method if not null`() {
|
||||||
|
val spyFactory = spy(factory)
|
||||||
|
val dataSource = mock<SearchDataSource<String>>()
|
||||||
|
Mockito.doReturn(dataSource).`when`(spyFactory).create()
|
||||||
|
factory.retryFailedRequest()
|
||||||
|
verify(dataSource).retryFailedRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest does not invoke method if null`() {
|
||||||
|
factory.retryFailedRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
package fr.free.nrw.commons.explore
|
||||||
|
|
||||||
|
import androidx.paging.PositionalDataSource
|
||||||
|
import com.nhaarman.mockitokotlin2.*
|
||||||
|
import fr.free.nrw.commons.explore.depictions.LoadingStates
|
||||||
|
import io.reactivex.plugins.RxJavaPlugins
|
||||||
|
import io.reactivex.processors.PublishProcessor
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.mockito.Mock
|
||||||
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
class SearchDataSourceTest {
|
||||||
|
|
||||||
|
private lateinit var loadingStates: PublishProcessor<LoadingState>
|
||||||
|
private lateinit var searchDepictionsDataSource: TestSearchDataSource
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private lateinit var mockGetItems: MockGetItems
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
|
||||||
|
MockitoAnnotations.initMocks(this)
|
||||||
|
loadingStates = PublishProcessor.create()
|
||||||
|
searchDepictionsDataSource =
|
||||||
|
TestSearchDataSource(
|
||||||
|
loadingStates,
|
||||||
|
mockGetItems
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
RxJavaPlugins.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadInitial returns results and emits InitialLoad & Complete`() {
|
||||||
|
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
|
||||||
|
val callback = mock<PositionalDataSource.LoadInitialCallback<String>>()
|
||||||
|
whenever(mockGetItems.getItems(1, 0)).thenReturn(emptyList())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadInitial(params, callback)
|
||||||
|
verify(callback).onResult(emptyList(), 0)
|
||||||
|
testSubscriber.assertValues(LoadingState.InitialLoad, LoadingState.Complete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadInitial onError does not return results and emits InitialLoad & Error`() {
|
||||||
|
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
|
||||||
|
val callback = mock<PositionalDataSource.LoadInitialCallback<String>>()
|
||||||
|
whenever(mockGetItems.getItems(1, 0)).thenThrow(RuntimeException())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadInitial(params, callback)
|
||||||
|
verify(callback, never()).onResult(any(), any())
|
||||||
|
testSubscriber.assertValues(LoadingState.InitialLoad, LoadingState.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadRange returns results and emits Loading & Complete`() {
|
||||||
|
val callback: PositionalDataSource.LoadRangeCallback<String> = mock()
|
||||||
|
val params = PositionalDataSource.LoadRangeParams(0, 1)
|
||||||
|
whenever(mockGetItems.getItems(params.loadSize, params.startPosition))
|
||||||
|
.thenReturn(emptyList())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadRange(params, callback)
|
||||||
|
verify(callback).onResult(emptyList())
|
||||||
|
testSubscriber.assertValues(LoadingState.Loading, LoadingState.Complete)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadRange onError does not return results and emits Loading & Error`() {
|
||||||
|
val callback: PositionalDataSource.LoadRangeCallback<String> = mock()
|
||||||
|
val params = PositionalDataSource.LoadRangeParams(0, 1)
|
||||||
|
whenever(mockGetItems.getItems(params.loadSize, params.startPosition))
|
||||||
|
.thenThrow(RuntimeException())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadRange(params, callback)
|
||||||
|
verify(callback, never()).onResult(any())
|
||||||
|
testSubscriber.assertValues(LoadingState.Loading, LoadingState.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest does nothing when null`() {
|
||||||
|
searchDepictionsDataSource.retryFailedRequest()
|
||||||
|
verifyNoMoreInteractions(mockGetItems)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `retryFailedRequest retries last request`() {
|
||||||
|
val callback: PositionalDataSource.LoadRangeCallback<String> = mock()
|
||||||
|
val params = PositionalDataSource.LoadRangeParams(0, 1)
|
||||||
|
whenever(mockGetItems.getItems(params.loadSize, params.startPosition))
|
||||||
|
.thenThrow(RuntimeException()).thenReturn(emptyList())
|
||||||
|
val testSubscriber = loadingStates.test()
|
||||||
|
searchDepictionsDataSource.loadRange(params, callback)
|
||||||
|
verify(callback, never()).onResult(any())
|
||||||
|
searchDepictionsDataSource.retryFailedRequest()
|
||||||
|
verify(callback).onResult(emptyList())
|
||||||
|
testSubscriber.assertValues(
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Error,
|
||||||
|
LoadingState.Loading,
|
||||||
|
LoadingState.Complete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestSearchDataSource(loadingStates: LoadingStates, val mockGetItems: MockGetItems) :
|
||||||
|
SearchDataSource<String>(loadingStates) {
|
||||||
|
override fun getItems(loadSize: Int, startPosition: Int): List<String> =
|
||||||
|
mockGetItems.getItems(loadSize, startPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MockGetItems {
|
||||||
|
fun getItems(loadSize: Int, startPosition: Int): List<String>
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.explore.categroies
|
||||||
|
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import fr.free.nrw.commons.category.CategoryClient
|
||||||
|
import fr.free.nrw.commons.explore.categories.PageableCategoriesDataSource
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PageableCategoriesDataSourceTest {
|
||||||
|
@Test
|
||||||
|
fun `loadFunction loads categories`() {
|
||||||
|
val categoryClient: CategoryClient = mock()
|
||||||
|
whenever(categoryClient.searchCategories("test", 0, 1))
|
||||||
|
.thenReturn(Observable.just(emptyList()))
|
||||||
|
val pageableCategoriesDataSource = PageableCategoriesDataSource(mock(), categoryClient)
|
||||||
|
pageableCategoriesDataSource.onQueryUpdated("test")
|
||||||
|
assertThat(pageableCategoriesDataSource.loadFunction(0, 1), Matchers.`is`(emptyList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
package fr.free.nrw.commons.explore.depictions
|
||||||
|
|
||||||
|
import com.nhaarman.mockitokotlin2.mock
|
||||||
|
import com.nhaarman.mockitokotlin2.whenever
|
||||||
|
import io.reactivex.Single
|
||||||
|
import org.hamcrest.MatcherAssert.assertThat
|
||||||
|
import org.hamcrest.Matchers
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class PageableDepictionsDataSourceTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `loadFunction loads depictions`() {
|
||||||
|
val depictsClient: DepictsClient = mock()
|
||||||
|
whenever(depictsClient.searchForDepictions("test", 0, 1)).thenReturn(Single.just(emptyList()))
|
||||||
|
val pageableDepictionsDataSource = PageableDepictionsDataSource(mock(), depictsClient)
|
||||||
|
pageableDepictionsDataSource.onQueryUpdated("test")
|
||||||
|
assertThat(pageableDepictionsDataSource.loadFunction.invoke(0, 1), Matchers.`is`(emptyList()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue