#3822 Convert SubCategoryImagesListFragment to use Pagination - convert parent categories - delete list fragment - creat base class to support continuation requests in clients

This commit is contained in:
Sean Mac Gillicuddy 2020-06-19 09:38:05 +01:00
parent 5b87ed569c
commit 0f091600e8
30 changed files with 188 additions and 410 deletions

View file

@ -140,9 +140,6 @@ public class BookmarksActivity extends NavigationBaseActivity
return adapter.getMediaAdapter().getCount();
}
@Override
public void requestMoreImages() { }
@Override
public void onMediaClicked(int position) {
//TODO use with pagination

View file

@ -7,15 +7,14 @@ import javax.inject.Singleton
const val CATEGORY_PREFIX = "Category:"
const val SUB_CATEGORY_CONTINUATION_PREFIX = "sub_category_"
const val PARENT_CATEGORY_CONTINUATION_PREFIX = "parent_category_"
/**
* Category Client to handle custom calls to Commons MediaWiki APIs
*/
@Singleton
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) {
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) :
ContinuationClient<MwQueryResponse, String>() {
/**
* Searches for categories containing the specified string.
@ -28,9 +27,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
@JvmOverloads
fun searchCategories(filter: String?, itemLimit: Int, offset: Int = 0):
Single<List<String>> {
return responseToCategoryName(
categoryInterface.searchCategories(filter, itemLimit, offset)
)
return responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset))
}
/**
@ -44,7 +41,7 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
@JvmOverloads
fun searchCategoriesForPrefix(prefix: String?, itemLimit: Int, offset: Int = 0):
Single<List<String>> {
return responseToCategoryName(
return responseMapper(
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset)
)
}
@ -56,18 +53,11 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @param categoryName Category name as defined on commons
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/
fun getSubCategoryList(categoryName: String?): Single<List<String>> {
val key = "$SUB_CATEGORY_CONTINUATION_PREFIX$categoryName"
return if (hasMorePagesFor(key)) {
responseToCategoryName(
categoryInterface.getSubCategoryList(
categoryName,
continuationStore[key] ?: emptyMap()
),
key
fun getSubCategoryList(categoryName: String): Single<List<String>> {
return continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) {
categoryInterface.getSubCategoryList(
categoryName, it
)
} else {
Single.just(emptyList())
}
}
@ -78,29 +68,27 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @param categoryName Category name as defined on commons
* @return
*/
fun getParentCategoryList(categoryName: String?): Single<List<String>> {
return responseToCategoryName(categoryInterface.getParentCategoryList(categoryName))
fun getParentCategoryList(categoryName: String): Single<List<String>> {
return continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) {
categoryInterface.getParentCategoryList(categoryName, it)
}
}
/**
* Internal function to reduce code reuse. Extracts the categories returned from MwQueryResponse.
*
* @param responseObservable The query response observable
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/
private fun responseToCategoryName(
responseObservable: Single<MwQueryResponse>,
key: String? = null
fun resetSubCategoryContinuation(category: String) {
resetContinuation(SUB_CATEGORY_CONTINUATION_PREFIX, category)
}
fun resetParentCategoryContinuation(category: String) {
resetContinuation(PARENT_CATEGORY_CONTINUATION_PREFIX, category)
}
override fun responseMapper(
networkResult: Single<MwQueryResponse>,
key: String?
): Single<List<String>> {
return responseObservable
return networkResult
.map {
if (key != null) {
continuationExists[key] =
it.continuation()?.let { continuation ->
continuationStore[key] = continuation
true
} ?: false
}
handleContinuationResponse(it.continuation(), key)
it.query()?.pages() ?: emptyList()
}
.map {
@ -108,10 +96,4 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
}
}
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
fun resetSubCategoryContinuation(category: String) {
continuationExists.remove("$SUB_CATEGORY_CONTINUATION_PREFIX$category")
continuationStore.remove("$SUB_CATEGORY_CONTINUATION_PREFIX$category")
}
}

View file

@ -20,6 +20,7 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
@ -71,23 +72,20 @@ public class CategoryDetailsActivity extends NavigationBaseActivity
List<String> titleList = new ArrayList<>();
categoriesMediaFragment = new CategoriesMediaFragment();
SubCategoriesFragment subCategoryListFragment = new SubCategoriesFragment();
SubCategoryListFragment parentCategoryListFragment = new SubCategoryListFragment();
ParentCategoriesFragment parentCategoriesFragment = new ParentCategoriesFragment();
categoryName = getIntent().getStringExtra("categoryName");
if (getIntent() != null && categoryName != null) {
Bundle arguments = new Bundle();
arguments.putString("categoryName", categoryName);
categoriesMediaFragment.setArguments(arguments);
subCategoryListFragment.setArguments(arguments);
Bundle parentCategoryArguments = new Bundle();
parentCategoryArguments.putString("categoryName", categoryName);
parentCategoryArguments.putBoolean("isParentCategory", true);
parentCategoryListFragment.setArguments(parentCategoryArguments);
parentCategoriesFragment.setArguments(arguments);
}
fragmentList.add(categoriesMediaFragment);
titleList.add("MEDIA");
fragmentList.add(subCategoryListFragment);
titleList.add("SUBCATEGORIES");
fragmentList.add(parentCategoryListFragment);
fragmentList.add(parentCategoriesFragment);
titleList.add("PARENT CATEGORIES");
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
@ -133,7 +131,6 @@ public class CategoryDetailsActivity extends NavigationBaseActivity
*/
public static void startYourself(Context context, String categoryName) {
Intent intent = new Intent(context, CategoryDetailsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra("categoryName", categoryName);
context.startActivity(intent);
}
@ -212,12 +209,4 @@ public class CategoryDetailsActivity extends NavigationBaseActivity
}
}
/**
* This method is called when viewPager has reached its end.
* Fetches more images using search query and adds it to the grid view and viewpager adapter
*/
@Override
public void requestMoreImages() {
//unneeded
}
}

View file

@ -187,15 +187,6 @@ public class CategoryImagesActivity
}
}
/**
* This method is called when viewPager has reached its end.
* Fetches more images using search query and adds it to the gridView and viewpager adapter
*/
@Override
public void requestMoreImages() {
//unneeded
}
@Override
public void onMediaClicked(int position) {
// this class is unused and will be deleted

View file

@ -7,7 +7,6 @@ package fr.free.nrw.commons.category;
public interface CategoryImagesCallback {
void viewPagerNotifyDataSetChanged();
void requestMoreImages();
void onMediaClicked(int position);
}

View file

@ -43,7 +43,8 @@ public interface CategoryInterface {
@QueryMap(encoded = true) Map<String, String> continuation);
@GET("w/api.php?action=query&format=json&formatversion=2"
+ "&generator=categories&prop=info&gcllimit=500")
Single<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName);
+ "&generator=categories&prop=info&gcllimit=50")
Single<MwQueryResponse> getParentCategoryList(@Query("titles") String categoryName,
@QueryMap(encoded = true) Map<String, String> continuation);
}

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.category
import io.reactivex.Single
abstract class ContinuationClient<Network, Domain> {
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
fun continuationRequest(
prefix: String,
name: String,
requestFunction: (Map<String, String>) -> Single<Network>
): Single<List<Domain>> {
val key = "$prefix$name"
return if (hasMorePagesFor(key)) {
responseMapper(requestFunction(continuationStore[key] ?: emptyMap()), key)
} else {
Single.just(emptyList())
}
}
abstract fun responseMapper(networkResult: Single<Network>, key: String?=null): Single<List<Domain>>
fun handleContinuationResponse(continuation:Map<String,String>?, key:String?){
if (key != null) {
continuationExists[key] =
continuation?.let { continuation ->
continuationStore[key] = continuation
true
} ?: false
}
}
protected fun resetContinuation(prefix: String, category: String) {
continuationExists.remove("$prefix$category")
continuationStore.remove("$prefix$category")
}
}

View file

@ -1,154 +0,0 @@
package fr.free.nrw.commons.category;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX;
import android.content.Intent;
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.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.categories.search.SearchCategoriesAdapter;
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.List;
import javax.inject.Inject;
import kotlin.Unit;
import timber.log.Timber;
/**
* Displays the category search screen.
*/
public class SubCategoryListFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.imagesListBox)
RecyclerView categoriesRecyclerView;
@BindView(R.id.imageSearchInProgress)
ProgressBar progressBar;
@BindView(R.id.imagesNotFound)
TextView categoriesNotFoundView;
private String categoryName = null;
@Inject CategoryClient categoryClient;
private SearchCategoriesAdapter categoriesAdapter;
private boolean isParentCategory = true;
@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);
categoryName = getArguments().getString("categoryName");
isParentCategory = getArguments().getBoolean("isParentCategory");
initSubCategoryList();
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
}
else{
categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2));
}
categoriesAdapter = new SearchCategoriesAdapter(item->{
Intent intent = new Intent(getContext(), CategoryDetailsActivity.class);
intent.putExtra("categoryName", item);
getContext().startActivity(intent);
return Unit.INSTANCE;
});
categoriesRecyclerView.setAdapter(categoriesAdapter);
return rootView;
}
/**
* Checks for internet connection and then initializes the recycler view with all(max 500) categories of the searched query
* Clearing categoryAdapter every time new keyword is searched so that user can see only new results
*/
public void initSubCategoryList() {
categoriesNotFoundView.setVisibility(GONE);
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
progressBar.setVisibility(View.VISIBLE);
if (isParentCategory) {
compositeDisposable.add(categoryClient.getParentCategoryList(
CATEGORY_PREFIX +categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleSuccess, this::handleError));
} else {
compositeDisposable.add(categoryClient.getSubCategoryList(
CATEGORY_PREFIX +categoryName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleSuccess, this::handleError));
}
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
* @param subCategoryList
*/
private void handleSuccess(List<String> subCategoryList) {
if (subCategoryList == null || subCategoryList.isEmpty()) {
initEmptyView();
}
else {
progressBar.setVisibility(View.GONE);
categoriesAdapter.addAll(subCategoryList);
categoriesAdapter.notifyDataSetChanged();
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
if (!isParentCategory){
Timber.e(throwable, "Error occurred while loading queried subcategories");
ViewUtil.showShortSnackbar(categoriesRecyclerView,R.string.error_loading_categories);
}else {
Timber.e(throwable, "Error occurred while loading queried parentcategories");
ViewUtil.showShortSnackbar(categoriesRecyclerView,R.string.error_loading_categories);
}
}
/**
* Handles the UI updates for a empty results scenario
*/
private void initEmptyView() {
progressBar.setVisibility(GONE);
categoriesNotFoundView.setVisibility(VISIBLE);
if (!isParentCategory){
categoriesNotFoundView.setText(getString(R.string.no_subcategory_found));
}else {
categoriesNotFoundView.setText(getString(R.string.no_parentcategory_found));
}
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
progressBar.setVisibility(GONE);
ViewUtil.showShortSnackbar(categoriesRecyclerView, R.string.no_internet);
}
}

View file

@ -51,9 +51,6 @@ class ContributionBoundaryCallback @Inject constructor(
* Fetches contributions using the MediaWiki API
*/
fun fetchContributions() {
if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName!!).not()) {
return
}
compositeDisposable.add(
mediaClient.getMediaListForUser(sessionManager.userName!!)
.map { mediaList: List<Media?> ->

View file

@ -4,15 +4,15 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.category.SubCategoryListFragment;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment;
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
@ -29,7 +29,7 @@ import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
/**
* This Class Represents the Module for dependency injection (using dagger)
* so, if a developer needs to add a new Fragment to the commons app
* then that must be mentioned here to inject the dependencies
* then that must be mentioned here to inject the dependencies
*/
@Module
@SuppressWarnings({"WeakerAccess", "unused"})
@ -50,9 +50,6 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract DepictedImagesFragment bindDepictedImagesFragment();
@ContributesAndroidInjector
abstract SubCategoryListFragment bindSubCategoryListFragment();
@ContributesAndroidInjector
abstract SearchMediaFragment bindBrowseImagesListFragment();
@ -103,4 +100,7 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract SubCategoriesFragment bindSubCategoriesFragment();
@ContributesAndroidInjector
abstract ParentCategoriesFragment bindParentCategoriesFragment();
}

View file

@ -161,16 +161,6 @@ public class ExploreActivity
super.onBackPressed();
}
/**
* This method is called when viewPager has reached its end.
* Fetches more images and adds them to the recycler view and viewpager adapter
*/
@Override
public void requestMoreImages() {
//unneeded
}
/**
* This method is called onClick of media inside category featured images or mobile uploads.
*/

View file

@ -264,14 +264,6 @@ public class SearchActivity extends NavigationBaseActivity
viewPager.requestFocus();
}
/**
* This method is called when viewPager has reached its end.
* Fetches more images using search query and adds it to the recycler view and viewpager adapter
*/
@Override
public void requestMoreImages() {
//unneeded
}
@Override protected void onDestroy() {
super.onDestroy();

View file

@ -4,6 +4,8 @@ import dagger.Binds
import dagger.Module
import fr.free.nrw.commons.explore.categories.media.CategoryMediaPresenter
import fr.free.nrw.commons.explore.categories.media.CategoryMediaPresenterImpl
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesPresenter
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesPresenterImpl
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenter
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenterImpl
@ -18,4 +20,8 @@ abstract class CategoriesModule {
@Binds
abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter()
: SubCategoriesPresenter
@Binds
abstract fun ParentCategoriesPresenterImpl.bindsParentCategoriesPresenter()
: ParentCategoriesPresenter
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore.categories.search
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryDetailsActivity

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.explore.categories.search
package fr.free.nrw.commons.explore.categories
import android.view.View
import android.view.ViewGroup

View file

@ -0,0 +1,19 @@
package fr.free.nrw.commons.explore.categories.parent
import fr.free.nrw.commons.category.CategoryClient
import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import javax.inject.Inject
class PageableParentCategoriesDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient
) : PageableBaseDataSource<String>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int ->
if (startPosition == 0) {
categoryClient.resetParentCategoryContinuation(query)
}
categoryClient.getParentCategoryList(query).blockingGet()
}
}

View file

@ -0,0 +1,26 @@
package fr.free.nrw.commons.explore.categories.parent
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
import javax.inject.Inject
class ParentCategoriesFragment : PageableCategoryFragment() {
@Inject
lateinit var presenter: ParentCategoriesPresenter
override val injectedPresenter
get() = presenter
override fun getEmptyText(query: String) = getString(R.string.no_parentcategory_found)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.explore.categories.parent
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.paging.BasePagingPresenter
import fr.free.nrw.commons.explore.paging.PagingContract
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
interface ParentCategoriesPresenter : PagingContract.Presenter<String>
class ParentCategoriesPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableParentCategoriesDataSource
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
ParentCategoriesPresenter

View file

@ -5,7 +5,7 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import javax.inject.Inject
class PageableCategoriesDataSource @Inject constructor(
class PageableSearchCategoriesDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient
) : PageableBaseDataSource<String>(liveDataConverter) {

View file

@ -1,11 +0,0 @@
package fr.free.nrw.commons.explore.categories.search
import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter
class SearchCategoriesAdapter(onCateoryClicked: (String) -> Unit) : BaseDelegateAdapter<String>(
searchCategoryDelegate(
onCateoryClicked
),
areItemsTheSame = { oldItem, newItem -> oldItem == newItem }
)

View file

@ -1,15 +0,0 @@
package fr.free.nrw.commons.explore.categories.search
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CATEGORY_PREFIX
import kotlinx.android.synthetic.main.item_recent_searches.*
fun searchCategoryDelegate(onCategoryClicked: (String) -> Unit) =
adapterDelegateLayoutContainer<String, String>(R.layout.item_recent_searches) {
containerView.setOnClickListener { onCategoryClicked(item) }
bind {
textView1.text = item.substringAfter(CATEGORY_PREFIX)
}
}

View file

@ -11,6 +11,6 @@ interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String>
class SearchCategoriesFragmentPresenterImpl @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableCategoriesDataSource
dataSourceFactory: PageableSearchCategoriesDataSource
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
SearchCategoriesFragmentPresenter

View file

@ -1,8 +1,7 @@
package fr.free.nrw.commons.explore.categories.search
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryDetailsActivity
import fr.free.nrw.commons.explore.paging.BasePagingFragment
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
import javax.inject.Inject
/**

View file

@ -4,8 +4,7 @@ import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.categories.search.PageableCategoryFragment
import fr.free.nrw.commons.explore.paging.PagingContract
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
import javax.inject.Inject
@ -13,7 +12,7 @@ class SubCategoriesFragment : PageableCategoryFragment() {
@Inject lateinit var presenter: SubCategoriesPresenter
override val injectedPresenter: PagingContract.Presenter<String>
override val injectedPresenter
get() = presenter
override fun getEmptyText(query: String) = getString(R.string.no_subcategory_found)

View file

@ -13,6 +13,7 @@ import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
@ -26,7 +27,8 @@ import java.util.List;
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
*/
public class WikidataItemDetailsActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider {
public class WikidataItemDetailsActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider,
CategoryImagesCallback {
private FragmentManager supportFragmentManager;
private DepictedImagesFragment depictionImagesListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
@ -73,13 +75,13 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
* This method is called on success of API call for featured Images.
* The viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetailPagerFragment !=null){
mediaDetailPagerFragment.notifyDataSetChanged();
}
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.

View file

@ -14,9 +14,7 @@ import kotlinx.android.synthetic.main.fragment_search_paginated.*
abstract class PageableMediaFragment : BasePagingFragment<Media>(), MediaDetailProvider {
override val pagedListAdapter by lazy {
PagedMediaAdapter {
categoryImagesCallback.onMediaClicked(it)
}
PagedMediaAdapter(categoryImagesCallback::onMediaClicked)
}
override val errorTextId: Int = R.string.error_loading_images
@ -30,9 +28,8 @@ abstract class PageableMediaFragment : BasePagingFragment<Media>(), MediaDetailP
categoryImagesCallback = (context as CategoryImagesCallback)
}
private val simpleDataObserver = SimpleDataObserver {
categoryImagesCallback.viewPagerNotifyDataSetChanged()
}
private val simpleDataObserver =
SimpleDataObserver { categoryImagesCallback.viewPagerNotifyDataSetChanged() }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.media
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.category.ContinuationClient
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Single
@ -24,14 +25,10 @@ class MediaClient @Inject constructor(
private val pageMediaInterface: PageMediaInterface,
private val mediaDetailInterface: MediaDetailInterface,
private val mediaConverter: MediaConverter
) {
) : ContinuationClient<MwQueryResponse, Media>() {
fun getMediaById(id: String) =
responseToMediaList(mediaInterface.getMediaById(id)).map { it.first() }
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
responseMapper(mediaInterface.getMediaById(id)).map { it.first() }
/**
* Checks if a page exists on Commons
@ -62,18 +59,8 @@ class MediaClient @Inject constructor(
* @return
*/
fun getMediaListFromCategory(category: String): Single<List<Media>> {
val key = "$CATEGORY_CONTINUATION_PREFIX$category"
return if (hasMorePagesFor(key)) {
responseToMediaList(
mediaInterface.getMediaListFromCategory(
category,
10,
continuationStore[key] ?: emptyMap()
),
key
)
} else {
Single.just(emptyList())
return continuationRequest(CATEGORY_CONTINUATION_PREFIX, category) {
mediaInterface.getMediaListFromCategory(category, 10, it)
}
}
@ -86,17 +73,11 @@ class MediaClient @Inject constructor(
* @return
*/
fun getMediaListForUser(userName: String): Single<List<Media>> {
return responseToMediaList(
mediaInterface.getMediaListForUser(
userName,
10,
continuationStore["user_$userName"] ?: Collections.emptyMap()
),
"user_$userName"
)
return continuationRequest("user_", userName) {
mediaInterface.getMediaListForUser(userName, 10, it)
}
}
/**
* This method takes a keyword as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time.
@ -107,7 +88,7 @@ class MediaClient @Inject constructor(
* @return
*/
fun getMediaListFromSearch(keyword: String?, limit: Int, offset: Int) =
responseToMediaList(mediaInterface.getMediaListFromSearch(keyword, limit, offset))
responseMapper(mediaInterface.getMediaListFromSearch(keyword, limit, offset))
/**
* @return list of images for a particular depict entity
@ -117,7 +98,7 @@ class MediaClient @Inject constructor(
srlimit: Int,
sroffset: Int
): Single<List<Media>> {
return responseToMediaList(
return responseMapper(
mediaInterface.fetchImagesForDepictedItem(
"haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query,
srlimit.toString(),
@ -126,23 +107,6 @@ class MediaClient @Inject constructor(
)
}
private fun responseToMediaList(
response: Single<MwQueryResponse>,
key: String? = null
): Single<List<Media>> {
return response.map {
if (key != null) {
continuationExists[key] =
it.continuation()?.let { continuation ->
continuationStore[key] = continuation
true
} ?: false
}
it.query()?.pages() ?: emptyList()
}.flatMap(::mediaFromPageAndEntity)
}
private fun mediaFromPageAndEntity(pages: List<MwQueryPage>): Single<List<Media>> {
return if (pages.isEmpty())
Single.just(emptyList())
@ -165,7 +129,7 @@ class MediaClient @Inject constructor(
* @return
*/
fun getMedia(titles: String?): Single<Media> {
return responseToMediaList(mediaInterface.getMedia(titles))
return responseMapper(mediaInterface.getMedia(titles))
.map { it.first() }
}
@ -176,7 +140,7 @@ class MediaClient @Inject constructor(
*/
fun getPictureOfTheDay(): Single<Media> {
val date = CommonsDateUtil.getIso8601DateFormatShort().format(Date())
return responseToMediaList(mediaInterface.getMediaWithGenerator("Template:Potd/$date")).map { it.first() }
return responseMapper(mediaInterface.getMediaWithGenerator("Template:Potd/$date")).map { it.first() }
}
@ -193,24 +157,22 @@ class MediaClient @Inject constructor(
}
/**
* Check if media for user has reached the end of the list.
* @param userName
* @return
*/
fun doesMediaListForUserHaveMorePages(userName: String): Boolean {
return hasMorePagesFor("user_$userName")
}
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
fun doesPageContainMedia(title: String?): Single<Boolean> {
return pageMediaInterface.getMediaList(title)
.map { it.items.isNotEmpty() }
}
fun resetCategoryContinuation(category: String) {
continuationExists.remove("$CATEGORY_CONTINUATION_PREFIX$category")
continuationStore.remove("$CATEGORY_CONTINUATION_PREFIX$category")
resetContinuation(CATEGORY_CONTINUATION_PREFIX, category)
}
override fun responseMapper(
networkResult: Single<MwQueryResponse>,
key: String?
): Single<List<Media>> {
return networkResult.map {
handleContinuationResponse(it.continuation(), key)
it.query()?.pages() ?: emptyList()
}.flatMap(::mediaFromPageAndEntity)
}
}

View file

@ -13,7 +13,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentStatePagerAdapter;
@ -26,7 +25,6 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.utils.DownloadUtils;
@ -269,8 +267,6 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
Timber.d("Returning as activity is destroyed!");
return;
}
if (i+1 >= adapter.getCount() && getContext() instanceof CategoryImagesCallback)
((CategoryImagesCallback) getContext()).requestMoreImages();
getActivity().invalidateOptionsMenu();
}

View file

@ -1,44 +0,0 @@
<?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:focusableInTouchMode="true"
android:id="@+id/image_search_results_display"
android:orientation="vertical"
android:paddingTop="@dimen/tiny_gap"
>
<TextView
android:id="@+id/imagesNotFound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_centerInParent="true"
android:visibility="gone" />
<ProgressBar
android:id="@+id/bottomProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:paddingTop="@dimen/tiny_padding"
android:paddingBottom="@dimen/tiny_padding"
android:visibility="gone"
/>
<ProgressBar
android:id="@+id/imageSearchInProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/imagesListBox"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
android:layout_above="@+id/bottomProgressBar"
android:scrollbarSize="@dimen/standard_gap"
android:fadingEdge="none" />
</RelativeLayout>

View file

@ -3,19 +3,19 @@ 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.search.PageableCategoriesDataSource
import fr.free.nrw.commons.explore.categories.search.PageableSearchCategoriesDataSource
import io.reactivex.Single
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers
import org.junit.Test
class PageableCategoriesDataSourceTest {
class PageableSearchCategoriesDataSourceTest {
@Test
fun `loadFunction loads categories`() {
val categoryClient: CategoryClient = mock()
whenever(categoryClient.searchCategories("test", 0, 1))
.thenReturn(Single.just(emptyList()))
val pageableCategoriesDataSource = PageableCategoriesDataSource(mock(), categoryClient)
val pageableCategoriesDataSource = PageableSearchCategoriesDataSource(mock(), categoryClient)
pageableCategoriesDataSource.onQueryUpdated("test")
assertThat(pageableCategoriesDataSource.loadFunction(0, 1), Matchers.`is`(emptyList()))
}