#3810 Convert DepictedImagesFragment to use Pagination (#3815)

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadCategoryDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - update BaseAdapter to be easier to use

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchImagesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchCategoriesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace NotificationRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadDepictsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace PlaceRenderer

* #3756 Convert SearchDepictionsFragment to use Pagination - convert SearchDepictionsFragment

* #3756 Convert SearchDepictionsFragment to use Pagination - fix presenter unit tests now that view is not nullable - fix Category prefix imports

* #3756 Convert SearchDepictionsFragment to use Pagination - test DataSource related classes

* #3756 Convert SearchDepictionsFragment to use Pagination - reset rx scheduler - ignore failing test

* #3760 Convert SearchCategoriesFragment to use Pagination - extract functionality of pagination to base classes - add category pagination

* #3772 Convert SearchImagesFragment to use Pagination  - convert SearchImagesFragment - tidy up showing the empty view - make search fragments show snackbar with appropriate text

* #3772 Convert SearchImagesFragment to use Pagination  - allow viewpager to load more data

* #3760 remove test that got re-added by merge

* #3760 remove duplicate dependency

* #3772 fix compilation

* #3780 Create media using a combination of Entities & MwQueryResult - construct media with an entity - move fields from media down to contribution - move dynamic fields outside of media - remove unused constructors - remove all unnecessary fetching of captions/descriptions - bump database version

* #3808 Construct media objects that depict an item id correctly - use generator to construct media for DepictedImages

* #3810 Convert DepictedImagesFragment to use Pagination - extract common media paging methods - convert to DepictedImages to use pagination

* #3810 Convert DepictedImagesFragment to use Pagination - rename base classes to better reflect usage

* #3810 Convert DepictedImagesFragment to use Pagination - map to empty result with no pages

* #3810 Convert DepictedImagesFragment to use Pagination - align test with returned values

* #3780 Create media using a combination of Entities & MwQueryResult - update wikicode to align with expected behaviour

* #3780 Create media using a combination of Entities & MwQueryResult - replace old site of thumbnail title with most relevant caption
This commit is contained in:
Seán Mac Gillicuddy 2020-06-25 09:02:51 +01:00 committed by GitHub
parent 4b22583b60
commit 34ab6f581b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
45 changed files with 306 additions and 987 deletions

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons
import androidx.core.text.HtmlCompat
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.IdAndCaptions
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single

View file

@ -1,27 +0,0 @@
package fr.free.nrw.commons.depictions;
import dagger.Binds;
import dagger.Module;
import fr.free.nrw.commons.depictions.Media.DepictedImagesContract;
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter;
import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract;
import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter;
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
public abstract class DepictionModule {
@Binds
public abstract DepictedImagesContract.UserActionListener bindsDepictedImagesPresenter(
DepictedImagesPresenter
presenter
);
@Binds
public abstract SubDepictionListContract.UserActionListener bindsSubDepictionListPresenter(
SubDepictionListPresenter
presenter
);
}

View file

@ -0,0 +1,23 @@
package fr.free.nrw.commons.depictions
import dagger.Binds
import dagger.Module
import fr.free.nrw.commons.depictions.Media.DepictedImagesContract
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
import fr.free.nrw.commons.depictions.subClass.SubDepictionListContract
import fr.free.nrw.commons.depictions.subClass.SubDepictionListPresenter
/**
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
*/
@Module
abstract class DepictionModule {
@Binds
abstract fun SubDepictionListPresenter.bindsSubDepictionListPresenter()
: SubDepictionListContract.UserActionListener
@Binds
abstract fun DepictedImagesPresenter.bindsDepictedImagesContractPresenter()
: DepictedImagesContract.Presenter
}

View file

@ -1,119 +0,0 @@
package fr.free.nrw.commons.depictions;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import androidx.annotation.Nullable;
import com.facebook.drawee.view.SimpleDraweeView;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
/**
* Adapter for Items in DepictionDetailsActivity
*/
public class GridViewAdapter extends ArrayAdapter {
private List<Media> data;
public GridViewAdapter(Context context, int layoutResourceId, List<Media> data) {
super(context, layoutResourceId, data);
this.data = data;
}
/**
* Adds more item to the list
* Its triggered on scrolling down in the list
* @param images
*/
public void addItems(List<Media> images) {
if (data == null) {
data = new ArrayList<>();
}
data.addAll(images);
notifyDataSetChanged();
}
/**
* Check the first item in the new list with old list and returns true if they are same
* Its triggered on successful response of the fetch images API.
* @param images
*/
public boolean containsAll(List<Media> images){
if (images == null || images.isEmpty()) {
return false;
}
if (data == null) {
data = new ArrayList<>();
return false;
}
if (data.size() == 0) {
return false;
}
String fileName = data.get(0).getFilename();
String imageName = images.get(0).getFilename();
return imageName.equals(fileName);
}
@Override
public boolean isEmpty() {
return data == null || data.isEmpty();
}
/**
* Sets up the UI for the depicted image item
* @param position
* @param convertView
* @param parent
* @return
*/
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_depict_image, null);
}
Media item = data.get(position);
SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view);
TextView fileName = convertView.findViewById(R.id.depict_image_title);
TextView author = convertView.findViewById(R.id.depict_image_author);
fileName.setText(item.getDisplayTitle());
setAuthorView(item, author);
imageView.setImageURI(item.getThumbUrl());
return convertView;
}
@Nullable
@Override
public Media getItem(int position) {
return data.get(position);
}
/**
* Shows author information if its present
* @param item
* @param author
*/
private void setAuthorView(Media item, TextView author) {
if (!TextUtils.isEmpty(item.getCreator())) {
String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
String uploadedBy = String.format(Locale.getDefault(), uploadedByTemplate, item.getCreator());
author.setText(uploadedBy);
} else {
author.setVisibility(View.GONE);
}
}
}

View file

@ -1,98 +0,0 @@
package fr.free.nrw.commons.depictions.Media;
import android.widget.ListAdapter;
import java.util.List;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.Media;
/**
* Contract with which DepictedImagesFragment and its presenter will talk to each other
*/
public interface DepictedImagesContract {
interface View {
/**
* Handles the UI updates for no internet scenario
*/
void handleNoInternet();
/**
* Handles the UI updates for a error scenario
*/
void initErrorView();
/**
* Initializes the adapter with a list of Media objects
*
* @param mediaList List of new Media to be displayed
*/
void setAdapter(List<Media> mediaList);
/**
* Display snackbar
*/
void showSnackBar();
/**
* Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query
* @param isLastPage
*/
void setIsLastPage(boolean isLastPage);
/**
* Set visibility of progressbar depending on the boolean value
*/
void progressBarVisible(Boolean value);
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
*
* @return GridView Adapter
*/
ListAdapter getAdapter();
/**
* adds list to adapter
*/
void addItemsToAdapter(List<Media> media);
/**
* Sets loading status depending on the boolean value
*/
void setLoadingStatus(Boolean value);
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
*
* @param collection List of new Media to be displayed
*/
void handleSuccess(List<Media> collection);
}
interface UserActionListener extends BasePresenter<View> {
/**
* Checks for internet connection and then initializes the grid view with first 10 images of that depiction
*/
void initList(String entityId);
/**
* Fetches more images for the item and adds it to the grid view adapter
* @param entityId
*/
void fetchMoreImages(String entityId);
/**
* add items to query list
*/
void addItemsToQueryList(List<Media> collection);
}
}

View file

@ -0,0 +1,12 @@
package fr.free.nrw.commons.depictions.Media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.PagingContract
/**
* Contract with which DepictedImagesFragment and its presenter will talk to each other
*/
interface DepictedImagesContract {
interface View : PagingContract.View<Media>
interface Presenter : PagingContract.Presenter<Media>
}

View file

@ -1,249 +0,0 @@
package fr.free.nrw.commons.depictions.Media;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import butterknife.BindView;
import butterknife.ButterKnife;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.depictions.GridViewAdapter;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Fragment for showing image list after selected an item from SearchActivity In Explore
*/
public class DepictedImagesFragment extends DaggerFragment implements DepictedImagesContract.View {
public static final String PAGE_ID_PREFIX = "M";
@BindView(R.id.statusMessage)
TextView statusTextView;
@BindView(R.id.loadingImagesProgressBar)
ProgressBar progressBar;
@BindView(R.id.depicts_image_list)
GridView gridView;
@BindView(R.id.parentLayout)
RelativeLayout parentLayout;
@Inject
DepictedImagesPresenter presenter;
private GridViewAdapter gridAdapter;
private String entityId = null;
private boolean isLastPage;
private boolean isLoading = true;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_depict_image, container, false);
ButterKnife.bind(this, v);
presenter.onAttachView(this);
return v;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
initViews();
}
/**
* Initializes the UI elements for the fragment
* Setup the grid view to and scroll listener for it
*/
private void initViews() {
String depictsName = getArguments().getString("wikidataItemName");
entityId = getArguments().getString("entityId");
if (getArguments() != null && depictsName != null) {
initList();
setScrollListener();
}
}
private void initList() {
presenter.initList(entityId);
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
} else {
presenter.initList(entityId);
}
}
/**
* Handles the UI updates for no internet scenario
*/
@Override
public void handleNoInternet() {
progressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_internet));
} else {
ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet);
}
}
/**
* Handles the UI updates for a error scenario
*/
@Override
public void initErrorView() {
progressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
statusTextView.setVisibility(VISIBLE);
statusTextView.setText(getString(R.string.no_images_found));
} else {
statusTextView.setVisibility(GONE);
}
}
/**
* Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
* Checks if the item has more images before loading
* Also checks whether images are currently being fetched before triggering another request
*/
private void setScrollListener() {
gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
if (!isLastPage && !isLoading && (firstVisibleItem + visibleItemCount >= totalItemCount)) {
isLoading = true;
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
} else {
presenter.fetchMoreImages(entityId);
}
}
if (isLastPage) {
progressBar.setVisibility(GONE);
}
}
});
}
/**
* Display snackbar
*/
@Override
public void showSnackBar() {
ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images);
}
/**
* Set visibility of progressbar depending on the boolean value
*/
@Override
public void progressBarVisible(Boolean value) {
if (value) {
progressBar.setVisibility(VISIBLE);
} else {
progressBar.setVisibility(GONE);
}
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
*
* @return GridView Adapter
*/
@Override
public ListAdapter getAdapter() {
return gridAdapter;
}
/**
* Initializes the adapter with a list of Media objects
*
* @param mediaList List of new Media to be displayed
*/
@Override
public void setAdapter(List<Media> mediaList) {
gridAdapter = new fr.free.nrw.commons.depictions.GridViewAdapter(getContext(), R.layout.layout_depict_image, mediaList);
gridView.setAdapter(gridAdapter);
}
/**
* adds list to adapter
*/
@Override
public void addItemsToAdapter(List<Media> media) {
gridAdapter.addAll(media);
gridAdapter.notifyDataSetChanged();
}
/**
* Sets loading status depending on the boolean value
*/
@Override
public void setLoadingStatus(Boolean value) {
if (!value) {
statusTextView.setVisibility(GONE);
}
isLoading = value;
}
/**
* Inform the view that there are no more items to be loaded for this search query
* or reset the isLastPage for the current query
* @param isLastPage
*/
@Override
public void setIsLastPage(boolean isLastPage) {
this.isLastPage=isLastPage;
progressBar.setVisibility(GONE);
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
*
* @param collection List of new Media to be displayed
*/
@Override
public void handleSuccess(List<Media> collection) {
presenter.addItemsToQueryList(collection);
if (gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {
return;
}
gridAdapter.addItems(collection);
try {
((WikidataItemDetailsActivity) getContext()).viewPagerNotifyDataSetChanged();
} catch (RuntimeException e) {
Timber.e(e);
}
}
progressBar.setVisibility(GONE);
isLoading = false;
statusTextView.setVisibility(GONE);
}
}

View file

@ -0,0 +1,28 @@
package fr.free.nrw.commons.depictions.Media
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.explore.media.PageableMediaFragment
import javax.inject.Inject
class DepictedImagesFragment : PageableMediaFragment(), DepictedImagesContract.View {
@Inject
lateinit var presenter: DepictedImagesContract.Presenter
override val injectedPresenter
get() = presenter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onQueryUpdated(arguments!!.getString("entityId")!!)
}
override fun onItemClicked(position: Int) {
(activity as WikidataItemDetailsActivity).onMediaClicked(position)
}
override fun notifyViewPager() {
(activity as WikidataItemDetailsActivity).viewPagerNotifyDataSetChanged()
}
}

View file

@ -1,144 +0,0 @@
package fr.free.nrw.commons.depictions.Media;
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
import android.annotation.SuppressLint;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber;
/**
* Presenter for DepictedImagesFragment
*/
public class DepictedImagesPresenter implements DepictedImagesContract.UserActionListener {
private static final DepictedImagesContract.View DUMMY = (DepictedImagesContract.View) Proxy
.newProxyInstance(
DepictedImagesContract.View.class.getClassLoader(),
new Class[]{DepictedImagesContract.View.class},
(proxy, method, methodArgs) -> null);
MediaClient mediaClient;
@Named("default_preferences")
JsonKvStore depictionKvStore;
private final Scheduler ioScheduler;
private final Scheduler mainThreadScheduler;
private DepictedImagesContract.View view = DUMMY;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
/**
* Wikibase enitityId for the depicted Item
* Ex: Q9394
*/
private List<Media> queryList = new ArrayList<>();
@Inject
public DepictedImagesPresenter(@Named("default_preferences") JsonKvStore depictionKvStore,
MediaClient mediaClient,
@Named(IO_THREAD) Scheduler ioScheduler,
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
this.depictionKvStore = depictionKvStore;
this.ioScheduler = ioScheduler;
this.mainThreadScheduler = mainThreadScheduler;
this.mediaClient = mediaClient;
}
@Override
public void onAttachView(DepictedImagesContract.View view) {
this.view = view;
}
@Override
public void onDetachView() {
this.view = DUMMY;
}
/**
* Checks for internet connection and then initializes the grid view with first 10 images of that depiction
*/
@SuppressLint("CheckResult")
@Override
public void initList(String entityId) {
view.setLoadingStatus(true);
view.progressBarVisible(true);
view.setIsLastPage(false);
compositeDisposable.add(mediaClient.fetchImagesForDepictedItem(entityId, 0)
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Fetches more images for the item and adds it to the grid view adapter
* @param entityId
*/
@SuppressLint("CheckResult")
@Override
public void fetchMoreImages(String entityId) {
view.progressBarVisible(true);
compositeDisposable.add(mediaClient.fetchImagesForDepictedItem(entityId, queryList.size())
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(this::handlePaginationSuccess, this::handleError));
}
/**
* Handles the success scenario
* it initializes the recycler view by adding items to the adapter
*/
private void handlePaginationSuccess(List<Media> media) {
queryList.addAll(media);
view.progressBarVisible(false);
view.addItemsToAdapter(media);
}
/**
* Logs and handles API error scenario
*
* @param throwable
*/
public void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading images inside items");
try {
view.initErrorView();
view.showSnackBar();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection List of new Media to be displayed
*/
public void handleSuccess(List<Media> collection) {
if (collection == null || collection.isEmpty()) {
if (queryList.isEmpty()) {
view.initErrorView();
} else {
view.setIsLastPage(true);
}
} else {
this.queryList.addAll(collection);
view.handleSuccess(collection);
}
}
/**
* add items to query list
*/
@Override
public void addItemsToQueryList(List<Media> collection) {
queryList.addAll(collection);
}
}

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.depictions.Media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.BasePagingPresenter
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
/**
* Presenter for DepictedImagesFragment
*/
class DepictedImagesPresenter @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableDepictedMediaDataSource
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
DepictedImagesContract.Presenter

View file

@ -0,0 +1,17 @@
package fr.free.nrw.commons.depictions.Media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject
class PageableDepictedMediaDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
private val mediaClient: MediaClient
) : PageableBaseDataSource<Media>(liveDataConverter) {
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
mediaClient.fetchImagesForDepictedItem(query, loadSize, startPosition).blockingGet()
}
}

View file

@ -4,20 +4,13 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.AdapterView;
import android.widget.FrameLayout;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import java.util.ArrayList;
import java.util.List;
import butterknife.BindView;
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.depictions.Media.DepictedImagesFragment;
@ -26,11 +19,13 @@ import fr.free.nrw.commons.explore.ViewPagerAdapter;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList;
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, AdapterView.OnItemClickListener {
public class WikidataItemDetailsActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider {
private FragmentManager supportFragmentManager;
private DepictedImagesFragment depictionImagesListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
@ -121,11 +116,11 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
}
/**
* Shows media detail fragment when user clicks on any image in the list
*/
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
public void onMediaClicked(int position) {
tabLayout.setVisibility(View.GONE);
viewPager.setVisibility(View.GONE);
mediaContainer.setVisibility(View.VISIBLE);
@ -152,12 +147,7 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
*/
@Override
public Media getMediaAtPosition(int i) {
if (depictionImagesListFragment.getAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) depictionImagesListFragment.getAdapter().getItem(i);
}
return depictionImagesListFragment.getImageAtPosition(i);
}
/**
@ -182,10 +172,7 @@ public class WikidataItemDetailsActivity extends NavigationBaseActivity implemen
*/
@Override
public int getTotalMediaCount() {
if (depictionImagesListFragment.getAdapter() == null) {
return 0;
}
return depictionImagesListFragment.getAdapter().getCount();
return depictionImagesListFragment.getTotalImagesCount();
}
/**

View file

@ -20,12 +20,11 @@ import fr.free.nrw.commons.utils.ViewUtil
import kotlinx.android.synthetic.main.fragment_search_paginated.*
abstract class BaseSearchFragment<T> : CommonsDaggerSupportFragment(),
SearchFragmentContract.View<T> {
abstract class BasePagingFragment<T> : CommonsDaggerSupportFragment(),
PagingContract.View<T> {
abstract val pagedListAdapter: PagedListAdapter<T, *>
abstract val injectedPresenter: SearchFragmentContract.Presenter<T>
abstract val emptyTemplateTextId: Int
abstract val injectedPresenter: PagingContract.Presenter<T>
abstract val errorTextId: Int
private val loadingAdapter by lazy { FooterAdapter { injectedPresenter.retryFailedRequest() } }
private val mergeAdapter by lazy { MergeAdapter(pagedListAdapter, loadingAdapter) }
@ -49,11 +48,12 @@ abstract class BaseSearchFragment<T> : CommonsDaggerSupportFragment(),
)
}
override fun observeSearchResults(searchResults: LiveData<PagedList<T>>) {
override fun observePagingResults(searchResults: LiveData<PagedList<T>>) {
this.searchResults?.removeObservers(viewLifecycleOwner)
this.searchResults = searchResults
searchResults.observe(viewLifecycleOwner, Observer {
pagedListAdapter.submitList(it) })
pagedListAdapter.submitList(it)
})
}
override fun onAttach(context: Context) {
@ -61,7 +61,6 @@ abstract class BaseSearchFragment<T> : CommonsDaggerSupportFragment(),
injectedPresenter.onAttachView(this)
}
override fun onDetach() {
super.onDetach()
injectedPresenter.onDetachView()
@ -84,10 +83,12 @@ abstract class BaseSearchFragment<T> : CommonsDaggerSupportFragment(),
}
override fun showEmptyText(query: String) {
contentNotFound.text = getString(emptyTemplateTextId, query)
contentNotFound.text = getEmptyText(query)
contentNotFound.visibility = VISIBLE
}
abstract fun getEmptyText(query: String): String
override fun hideEmptyText() {
contentNotFound.visibility = GONE
}

View file

@ -7,25 +7,25 @@ import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
abstract class BaseSearchPresenter<T>(
abstract class BasePagingPresenter<T>(
val mainThreadScheduler: Scheduler,
val pageableDataSource: PageableDataSource<T>
) : SearchFragmentContract.Presenter<T> {
val pageableBaseDataSource: PageableBaseDataSource<T>
) : PagingContract.Presenter<T> {
private val DUMMY: SearchFragmentContract.View<T> = proxy()
private var view: SearchFragmentContract.View<T> = DUMMY
private val DUMMY: PagingContract.View<T> = proxy()
private var view: PagingContract.View<T> = DUMMY
private val compositeDisposable = CompositeDisposable()
override val listFooterData = MutableLiveData<List<FooterItem>>().apply { value = emptyList() }
override fun onAttachView(view: SearchFragmentContract.View<T>) {
override fun onAttachView(view: PagingContract.View<T>) {
this.view = view
compositeDisposable.addAll(
pageableDataSource.searchResults.subscribe(view::observeSearchResults),
pageableDataSource.loadingStates
pageableBaseDataSource.pagingResults.subscribe(view::observePagingResults),
pageableBaseDataSource.loadingStates
.observeOn(mainThreadScheduler)
.subscribe(::onLoadingState, Timber::e),
pageableDataSource.noItemsLoadedQueries.subscribe(view::showEmptyText)
pageableBaseDataSource.noItemsLoadedQueries.subscribe(view::showEmptyText)
)
}
@ -50,7 +50,7 @@ abstract class BaseSearchPresenter<T>(
}
override fun retryFailedRequest() {
pageableDataSource.retryFailedRequest()
pageableBaseDataSource.retryFailedRequest()
}
override fun onDetachView() {
@ -59,7 +59,7 @@ abstract class BaseSearchPresenter<T>(
}
override fun onQueryUpdated(query: String) {
pageableDataSource.onQueryUpdated(query)
pageableBaseDataSource.onQueryUpdated(query)
}
}

View file

@ -4,10 +4,10 @@ import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import fr.free.nrw.commons.BasePresenter
interface SearchFragmentContract {
interface PagingContract {
interface View<T> {
fun showSnackbar()
fun observeSearchResults(searchResults: LiveData<PagedList<T>>)
fun observePagingResults(searchResults: LiveData<PagedList<T>>)
fun showInitialLoadInProgress()
fun hideInitialLoadProgress()
fun showEmptyText(query: String)

View file

@ -14,25 +14,25 @@ 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) {
abstract class PageableBaseDataSource<T>(private val liveDataConverter: LiveDataConverter) {
lateinit var query: String
private val dataSourceFactoryFactory: () -> SearchDataSourceFactory<T> = {
private val dataSourceFactoryFactory: () -> PagingDataSourceFactory<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 _pagingResults = PublishProcessor.create<LiveData<PagedList<T>>>()
val pagingResults: Flowable<LiveData<PagedList<T>>> = _pagingResults
private val _noItemsLoadedEvent = PublishProcessor.create<String>()
val noItemsLoadedQueries: Flowable<String> = _noItemsLoadedEvent
private var currentFactory: SearchDataSourceFactory<T>? = null
private var currentFactory: PagingDataSourceFactory<T>? = null
abstract val loadFunction: LoadFunction<T>
fun onQueryUpdated(query: String) {
this.query = query
_searchResults.offer(
_pagingResults.offer(
liveDataConverter.convert(dataSourceFactoryFactory().also { currentFactory = it }) {
_noItemsLoadedEvent.offer(query)
}
@ -46,7 +46,7 @@ abstract class PageableDataSource<T>(private val liveDataConverter: LiveDataConv
class LiveDataConverter @Inject constructor() {
fun <T> convert(
dataSourceFactory: SearchDataSourceFactory<T>,
dataSourceFactory: PagingDataSourceFactory<T>,
zeroItemsLoadedFunction: () -> Unit
): LiveData<PagedList<T>> {
return dataSourceFactory.toLiveData(
@ -65,7 +65,7 @@ class LiveDataConverter @Inject constructor() {
}
abstract class SearchDataSourceFactory<T>(val loadingStates: LoadingStates) :
abstract class PagingDataSourceFactory<T>(val loadingStates: LoadingStates) :
DataSource.Factory<Int, T>() {
private var currentDataSource: SearchDataSource<T>? = null
abstract val loadFunction: LoadFunction<T>
@ -80,7 +80,7 @@ abstract class SearchDataSourceFactory<T>(val loadingStates: LoadingStates) :
}
fun <T> dataSourceFactory(loadingStates: LoadingStates, loadFunction: LoadFunction<T>) =
object : SearchDataSourceFactory<T>(loadingStates) {
object : PagingDataSourceFactory<T>(loadingStates) {
override val loadFunction: LoadFunction<T> = loadFunction
}

View file

@ -269,9 +269,7 @@ public class SearchActivity extends NavigationBaseActivity
*/
@Override
public void requestMoreImages() {
if (searchMediaFragment!=null){
searchMediaFragment.requestMoreImages();
}
//unneeded
}
@Override protected void onDestroy() {

View file

@ -10,5 +10,5 @@ import javax.inject.Named
class SearchCategoriesFragmentPresenter @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableCategoriesDataSource
) : BaseSearchPresenter<String>(mainThreadScheduler, dataSourceFactory),
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
SearchCategoriesFragmentContract.Presenter

View file

@ -14,18 +14,14 @@ import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenter
@Module
abstract class SearchModule {
@Binds
abstract fun bindsSearchDepictionsFragmentPresenter(
presenter: SearchDepictionsFragmentPresenter
): SearchDepictionsFragmentContract.Presenter
abstract fun SearchDepictionsFragmentPresenter.bindsSearchDepictionsFragmentPresenter()
: SearchDepictionsFragmentContract.Presenter
@Binds
abstract fun bindsSearchCategoriesFragmentPresenter(
presenter: SearchCategoriesFragmentPresenter
): SearchCategoriesFragmentContract.Presenter
abstract fun SearchCategoriesFragmentPresenter.bindsSearchCategoriesFragmentPresenter()
: SearchCategoriesFragmentContract.Presenter
@Binds
abstract fun bindsSearchMediaFragmentPresenter(
presenter: SearchMediaFragmentPresenter
): SearchMediaFragmentContract.Presenter
abstract fun SearchMediaFragmentPresenter.bindsSearchMediaFragmentPresenter()
: SearchMediaFragmentContract.Presenter
}

View file

@ -2,13 +2,13 @@ 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 fr.free.nrw.commons.explore.PageableBaseDataSource
import javax.inject.Inject
class PageableCategoriesDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient
) : PageableDataSource<String>(liveDataConverter) {
) : PageableBaseDataSource<String>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int ->
categoryClient.searchCategories(query, loadSize, startPosition).blockingFirst()

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.explore.categories
import fr.free.nrw.commons.explore.SearchFragmentContract
import fr.free.nrw.commons.explore.PagingContract
interface SearchCategoriesFragmentContract {
interface View : SearchFragmentContract.View<String>
interface Presenter : SearchFragmentContract.Presenter<String>
interface View : PagingContract.View<String>
interface Presenter : PagingContract.Presenter<String>
}

View file

@ -2,25 +2,25 @@ 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 fr.free.nrw.commons.explore.BasePagingFragment
import fr.free.nrw.commons.explore.PagingContract
import javax.inject.Inject
/**
* Displays the category search screen.
*/
class SearchCategoryFragment : BaseSearchFragment<String>() {
class SearchCategoryFragment : BasePagingFragment<String>() {
@Inject
lateinit var presenter: SearchCategoriesFragmentContract.Presenter
override val emptyTemplateTextId: Int = R.string.categories_not_found
override val errorTextId: Int = R.string.error_loading_categories
override val injectedPresenter: SearchFragmentContract.Presenter<String>
override val injectedPresenter
get() = presenter
override val pagedListAdapter by lazy {
PagedSearchCategoriesAdapter { CategoryDetailsActivity.startYourself(context, it) }
}
override fun getEmptyText(query: String) = getString(R.string.categories_not_found, query)
}

View file

@ -2,7 +2,7 @@ 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.explore.PageableBaseDataSource
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.processors.PublishProcessor
import javax.inject.Inject
@ -13,7 +13,7 @@ typealias LoadingStates = PublishProcessor<LoadingState>
class PageableDepictionsDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
val depictsClient: DepictsClient
) : PageableDataSource<DepictedItem>(liveDataConverter) {
) : PageableBaseDataSource<DepictedItem>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int ->
depictsClient.searchForDepictions(query, loadSize, startPosition).blockingGet()

View file

@ -2,26 +2,26 @@ package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.R
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity
import fr.free.nrw.commons.explore.BaseSearchFragment
import fr.free.nrw.commons.explore.BasePagingFragment
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject
/**
* Display depictions in search fragment
*/
class SearchDepictionsFragment : BaseSearchFragment<DepictedItem>(),
class SearchDepictionsFragment : BasePagingFragment<DepictedItem>(),
SearchDepictionsFragmentContract.View {
@Inject
lateinit var presenter: SearchDepictionsFragmentContract.Presenter
override val emptyTemplateTextId: Int = R.string.depictions_not_found
override val errorTextId: Int = R.string.error_loading_depictions
override val injectedPresenter: SearchDepictionsFragmentContract.Presenter
override val injectedPresenter
get() = presenter
override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
}
override fun getEmptyText(query: String) = getString(R.string.depictions_not_found, query)
}

View file

@ -1,12 +1,12 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.explore.SearchFragmentContract
import fr.free.nrw.commons.explore.PagingContract
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
/**
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
*/
interface SearchDepictionsFragmentContract {
interface View : SearchFragmentContract.View<DepictedItem>
interface Presenter : SearchFragmentContract.Presenter<DepictedItem>
interface View : PagingContract.View<DepictedItem>
interface Presenter : PagingContract.Presenter<DepictedItem>
}

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.explore.depictions
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.BaseSearchPresenter
import fr.free.nrw.commons.explore.BasePagingPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Scheduler
import javax.inject.Inject
@ -13,5 +13,5 @@ import javax.inject.Named
class SearchDepictionsFragmentPresenter @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableDepictionsDataSource
) : BaseSearchPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
) : BasePagingPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
SearchDepictionsFragmentContract.Presenter

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.LiveDataConverter
import fr.free.nrw.commons.explore.PageableDataSource
import fr.free.nrw.commons.explore.PageableBaseDataSource
import fr.free.nrw.commons.explore.depictions.LoadFunction
import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject
@ -10,7 +10,7 @@ import javax.inject.Inject
class PageableMediaDataSource @Inject constructor(
liveDataConverter: LiveDataConverter,
private val mediaClient: MediaClient
) : PageableDataSource<Media>(liveDataConverter) {
) : PageableBaseDataSource<Media>(liveDataConverter) {
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
mediaClient.getMediaListFromSearch(query, loadSize, startPosition).blockingGet()
}

View file

@ -0,0 +1,42 @@
package fr.free.nrw.commons.explore.media
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.BasePagingFragment
import kotlinx.android.synthetic.main.fragment_search_paginated.*
abstract class PageableMediaFragment : BasePagingFragment<Media>() {
override val pagedListAdapter by lazy { PagedMediaAdapter(::onItemClicked) }
override val errorTextId: Int = R.string.error_loading_images
override fun getEmptyText(query: String) = getString(R.string.no_images_found)
protected abstract fun onItemClicked(position: Int)
protected abstract fun notifyViewPager()
private val simpleDataObserver = SimpleDataObserver { notifyViewPager() }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagedListAdapter.registerAdapterDataObserver(simpleDataObserver)
}
override fun onDestroyView() {
super.onDestroyView()
pagedListAdapter.unregisterAdapterDataObserver(simpleDataObserver)
}
fun getImageAtPosition(position: Int): Media? =
pagedListAdapter.currentList?.get(position)?.takeIf { it.filename != null }
.also {
pagedListAdapter.currentList?.loadAround(position)
paginatedSearchResultsList.scrollToPosition(position)
}
fun getTotalImagesCount(): Int = pagedListAdapter.itemCount
}

View file

@ -10,7 +10,7 @@ import fr.free.nrw.commons.explore.BaseViewHolder
import fr.free.nrw.commons.explore.inflate
import kotlinx.android.synthetic.main.layout_category_images.*
class SearchImagesAdapter(private val onImageClicked: (Int) -> Unit) :
class PagedMediaAdapter(private val onImageClicked: (Int) -> Unit) :
PagedListAdapter<Media, SearchImagesViewHolder>(object : DiffUtil.ItemCallback<Media>() {
override fun areItemsTheSame(oldItem: Media, newItem: Media) =
oldItem.pageId == newItem.pageId

View file

@ -1,57 +1,26 @@
package fr.free.nrw.commons.explore.media
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.BaseSearchFragment
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.explore.SearchActivity
import javax.inject.Inject
/**
* Displays the image search screen.
*/
class SearchMediaFragment : BaseSearchFragment<Media>(), SearchMediaFragmentContract.View {
class SearchMediaFragment : PageableMediaFragment(), SearchMediaFragmentContract.View {
@Inject
lateinit var presenter: SearchMediaFragmentContract.Presenter
override val emptyTemplateTextId: Int = R.string.depictions_not_found
override val errorTextId: Int = R.string.error_loading_images
override val injectedPresenter: SearchMediaFragmentContract.Presenter
override val injectedPresenter
get() = presenter
override val pagedListAdapter by lazy {
SearchImagesAdapter {
(context as SearchActivity?)!!.onSearchImageClicked(it)
}
override fun onItemClicked(position: Int) {
(context as SearchActivity?)!!.onSearchImageClicked(position)
}
private val simpleDataObserver = SimpleDataObserver { notifyViewPager() }
fun requestMoreImages() {
// This functionality is replaced by a dataSetObserver and by using loadAround
override fun notifyViewPager() {
(activity as CategoryImagesCallback).viewPagerNotifyDataSetChanged()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
pagedListAdapter.registerAdapterDataObserver(simpleDataObserver)
}
override fun onDestroyView() {
super.onDestroyView()
pagedListAdapter.unregisterAdapterDataObserver(simpleDataObserver)
}
private fun notifyViewPager() {
(activity as SearchActivity).viewPagerNotifyDataSetChanged()
}
fun getImageAtPosition(position: Int): Media? =
pagedListAdapter.currentList?.get(position)?.takeIf { it.filename != null }
.also { pagedListAdapter.currentList?.loadAround(position) }
fun getTotalImagesCount(): Int = pagedListAdapter.itemCount
}

View file

@ -1,10 +1,10 @@
package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.explore.SearchFragmentContract
import fr.free.nrw.commons.explore.PagingContract
interface SearchMediaFragmentContract {
interface View : SearchFragmentContract.View<Media>
interface Presenter : SearchFragmentContract.Presenter<Media>
interface View : PagingContract.View<Media>
interface Presenter : PagingContract.Presenter<Media>
}

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.explore.media
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.explore.BaseSearchPresenter
import fr.free.nrw.commons.explore.BasePagingPresenter
import io.reactivex.Scheduler
import javax.inject.Inject
import javax.inject.Named
@ -10,5 +10,5 @@ import javax.inject.Named
class SearchMediaFragmentPresenter @Inject constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableMediaDataSource
) : BaseSearchPresenter<Media>(mainThreadScheduler, dataSourceFactory),
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
SearchMediaFragmentContract.Presenter

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.media
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX
import fr.free.nrw.commons.explore.media.MediaConverter
import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Single
@ -13,6 +12,8 @@ import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
const val PAGE_ID_PREFIX = "M"
/**
* Media Client to handle custom calls to Commons MediaWiki APIs
*/
@ -105,16 +106,20 @@ class MediaClient @Inject constructor(
/**
* @return list of images for a particular depict entity
*/
fun fetchImagesForDepictedItem(query: String, sroffset: Int): Single<List<Media>> {
fun fetchImagesForDepictedItem(
query: String,
srlimit: Int,
sroffset: Int
): Single<List<Media>> {
return responseToMediaList(
mediaInterface.fetchImagesForDepictedItem(
"haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query,
srlimit.toString(),
sroffset.toString()
)
)
}
private fun responseToMediaList(
response: Single<MwQueryResponse>,
key: String? = null
@ -133,11 +138,14 @@ class MediaClient @Inject constructor(
}
private fun mediaFromPageAndEntity(pages: List<MwQueryPage>): Single<List<Media>> {
return getEntities(pages.map { "$PAGE_ID_PREFIX${it.pageId()}" })
.map {
pages.zip(it.entities().values)
.map { (page, entity) -> mediaConverter.convert(page, entity) }
}
return if (pages.isEmpty())
Single.just(emptyList())
else
getEntities(pages.map { "$PAGE_ID_PREFIX${it.pageId()}" })
.map {
pages.zip(it.entities().values)
.map { (page, entity) -> mediaConverter.convert(page, entity) }
}
}
/**

View file

@ -117,14 +117,14 @@ public interface MediaInterface {
/**
* Fetches list of images from a depiction entity
*
* @param query depictionEntityId
* @param query depictionEntityId
* @param srlimit the number of items to fetch
* @param sroffset number od depictions already fetched, this is useful in implementing pagination
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=search&gsrnamespace=6" + //Search parameters
MEDIA_PARAMS)
Single<MwQueryResponse> fetchImagesForDepictedItem(@Query("gsrsearch") String query, @Query("gsroffset") String sroffset);
Single<MwQueryResponse> fetchImagesForDepictedItem(@Query("gsrsearch") String query,
@Query("gsrlimit")String srlimit, @Query("gsroffset") String sroffset);
}

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.wikidata;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX;
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
import fr.free.nrw.commons.upload.UploadResult;

View file

@ -1,6 +1,7 @@
package fr.free.nrw.commons.wikidata;
import static fr.free.nrw.commons.depictions.Media.DepictedImagesFragment.PAGE_ID_PREFIX;
import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX;
import android.annotation.SuppressLint;
import android.content.Context;

View file

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/parentLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@+id/mediaContainer"
android:background="?attr/mainBackground"
android:orientation="vertical">
<TextView
android:id="@+id/statusMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center"
android:text="@string/waiting_first_sync"
android:visibility="gone"
tools:visibility="visible" />
<ProgressBar
android:id="@+id/loadingImagesProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone" />
<GridView
android:id="@+id/depicts_image_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:columnWidth="240dp"
android:fadingEdge="none"
android:fastScrollEnabled="true"
android:listSelector="@null"
android:numColumns="auto_fit"
android:stretchMode="columnWidth" />
</RelativeLayout>

View file

@ -1,61 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="2dp"
android:paddingBottom="0dp">
<TextView
android:id="@+id/depict_images_sequence_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:textColor="#33FFFFFF"
android:textSize="98sp"
android:typeface="serif" />
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/depict_image_view"
android:layout_width="match_parent"
android:layout_height="240dp"
app:actualImageScaleType="centerCrop" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center|bottom"
android:background="#AA000000"
android:orientation="vertical"
android:padding="@dimen/small_gap">
<ProgressBar
android:id="@+id/depict_progress"
style="@style/ProgressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:indeterminateOnly="false"
android:max="100"
android:visibility="gone" />
<TextView
android:id="@+id/depict_image_title"
style="?android:textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#FFFFFFFF" />
<TextView
android:id="@+id/depict_image_author"
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="#FFFFFFFF" />
</LinearLayout>
</FrameLayout>

View file

@ -1,63 +0,0 @@
package fr.free.nrw.commons.depictions
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
class DepictedImagesPresenterTest {
@Mock
internal lateinit var view: DepictedImagesFragment
lateinit var depictedImagesPresenter: DepictedImagesPresenter
@Mock
lateinit var jsonKvStore: JsonKvStore
@Mock
lateinit var mediaClient: MediaClient
lateinit var testScheduler: TestScheduler
val mediaList: ArrayList<Media> = ArrayList()
@Mock
lateinit var mediaItem: Media
var testSingle: Single<List<Media>>? = null
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler()
mediaList.add(mediaItem)
testSingle = Single.just(mediaList)
depictedImagesPresenter = DepictedImagesPresenter(jsonKvStore,
mediaClient, testScheduler, testScheduler)
depictedImagesPresenter.onAttachView(view)
}
@Test
fun initList() {
Mockito.`when`(
mediaClient.fetchImagesForDepictedItem(ArgumentMatchers.anyString(),
ArgumentMatchers.anyInt())
).thenReturn(testSingle)
depictedImagesPresenter.initList("rabbit")
depictedImagesPresenter.handleSuccess(mediaList)
verify(view)?.handleSuccess(mediaList)
}
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.depictions.Media
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Single
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Test
class PageableDepictedMediaDataSourceTest{
@Test
fun `loadFunction loads Media`() {
val mediaClient = mock<MediaClient>()
whenever(mediaClient.fetchImagesForDepictedItem("test",0,1))
.thenReturn(Single.just(emptyList()))
val pageableDepictedMediaDataSource = PageableDepictedMediaDataSource(mock(), mediaClient)
pageableDepictedMediaDataSource.onQueryUpdated("test")
assertThat(pageableDepictedMediaDataSource.loadFunction(0,1), `is`(emptyList()))
}
}

View file

@ -15,21 +15,21 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class BaseSearchPresenterTest {
class BasePagingPresenterTest {
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
@Mock
internal lateinit var view: SearchFragmentContract.View<String>
internal lateinit var view: PagingContract.View<String>
private lateinit var baseSearchPresenter: BaseSearchPresenter<String>
private lateinit var basePagingPresenter: BasePagingPresenter<String>
private lateinit var testScheduler: TestScheduler
@Mock
private lateinit var pageableDataSource: PageableDataSource<String>
private lateinit var pageableBaseDataSource: PageableBaseDataSource<String>
private var loadingStates: PublishProcessor<LoadingState> = PublishProcessor.create()
@ -42,34 +42,34 @@ class BaseSearchPresenterTest {
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
whenever(pageableDataSource.searchResults).thenReturn(searchResults)
whenever(pageableDataSource.loadingStates).thenReturn(loadingStates)
whenever(pageableDataSource.noItemsLoadedQueries)
whenever(pageableBaseDataSource.pagingResults).thenReturn(searchResults)
whenever(pageableBaseDataSource.loadingStates).thenReturn(loadingStates)
whenever(pageableBaseDataSource.noItemsLoadedQueries)
.thenReturn(noItemLoadedQueries)
testScheduler = TestScheduler()
baseSearchPresenter =
object : BaseSearchPresenter<String>(testScheduler, pageableDataSource) {}
baseSearchPresenter.onAttachView(view)
basePagingPresenter =
object : BasePagingPresenter<String>(testScheduler, pageableBaseDataSource) {}
basePagingPresenter.onAttachView(view)
}
@Test
fun `searchResults emission updates the view`() {
val pagedListLiveData = mock<LiveData<PagedList<String>>>()
searchResults.offer(pagedListLiveData)
verify(view).observeSearchResults(pagedListLiveData)
verify(view).observePagingResults(pagedListLiveData)
}
@Test
fun `Loading offers a loading list item`() {
onLoadingState(LoadingState.Loading)
verify(view).hideEmptyText()
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.LoadingItem))
basePagingPresenter.listFooterData.test().assertValue(listOf(FooterItem.LoadingItem))
}
@Test
fun `Complete offers an empty list item and hides initial loader`() {
onLoadingState(LoadingState.Complete)
baseSearchPresenter.listFooterData.test()
basePagingPresenter.listFooterData.test()
.assertValue(emptyList())
verify(view).hideInitialLoadProgress()
}
@ -83,11 +83,11 @@ class BaseSearchPresenterTest {
@Test
fun `Error offers a refresh list item, hides initial loader and shows error with a set text`() {
baseSearchPresenter.onQueryUpdated("test")
basePagingPresenter.onQueryUpdated("test")
onLoadingState(LoadingState.Error)
verify(view).showSnackbar()
verify(view).hideInitialLoadProgress()
baseSearchPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem))
basePagingPresenter.listFooterData.test().assertValue(listOf(FooterItem.RefreshItem))
}
@Test
@ -98,21 +98,21 @@ class BaseSearchPresenterTest {
@Test
fun `retryFailedRequest calls retry`() {
baseSearchPresenter.retryFailedRequest()
verify(pageableDataSource).retryFailedRequest()
basePagingPresenter.retryFailedRequest()
verify(pageableBaseDataSource).retryFailedRequest()
}
@Test
fun `onDetachView stops subscriptions`() {
baseSearchPresenter.onDetachView()
basePagingPresenter.onDetachView()
onLoadingState(LoadingState.Loading)
baseSearchPresenter.listFooterData.test().assertValue(emptyList())
basePagingPresenter.listFooterData.test().assertValue(emptyList())
}
@Test
fun `onQueryUpdated updates dataSourceFactory`() {
baseSearchPresenter.onQueryUpdated("test")
verify(pageableDataSource).onQueryUpdated("test")
basePagingPresenter.onQueryUpdated("test")
verify(pageableBaseDataSource).onQueryUpdated("test")
}
private fun onLoadingState(loadingState: LoadingState) {

View file

@ -10,17 +10,17 @@ import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class PageableDataSourceTest {
class PageableBaseDataSourceTest {
@Mock
private lateinit var liveDataConverter: LiveDataConverter
private lateinit var pageableDataSource: PageableDataSource<String>
private lateinit var pageableBaseDataSource: PageableBaseDataSource<String>
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
pageableDataSource = object: PageableDataSource<String>(liveDataConverter){
pageableBaseDataSource = object: PageableBaseDataSource<String>(liveDataConverter){
override val loadFunction: LoadFunction<String>
get() = mock()
@ -30,16 +30,16 @@ class PageableDataSourceTest {
@Test
fun `onQueryUpdated emits new liveData`() {
val (_, liveData) = expectNewLiveData()
pageableDataSource.searchResults.test()
.also { pageableDataSource.onQueryUpdated("test") }
pageableBaseDataSource.pagingResults.test()
.also { pageableBaseDataSource.onQueryUpdated("test") }
.assertValue(liveData)
}
@Test
fun `onQueryUpdated invokes livedatconverter with no items emitter`() {
val (zeroItemsFuncCaptor, _) = expectNewLiveData()
pageableDataSource.onQueryUpdated("test")
pageableDataSource.noItemsLoadedQueries.test()
pageableBaseDataSource.onQueryUpdated("test")
pageableBaseDataSource.noItemsLoadedQueries.test()
.also { zeroItemsFuncCaptor.firstValue.invoke() }
.assertValue("test")
}
@ -49,22 +49,22 @@ class PageableDataSourceTest {
* */
@Test
fun `retryFailedRequest does nothing without a factory`() {
pageableDataSource.retryFailedRequest()
pageableBaseDataSource.retryFailedRequest()
}
@Test
@Ignore("Rewrite with Mockk constructor mocks")
fun `retryFailedRequest retries with a factory`() {
val (_, _, dataSourceFactoryCaptor) = expectNewLiveData()
pageableDataSource.onQueryUpdated("test")
pageableBaseDataSource.onQueryUpdated("test")
val dataSourceFactory = spy(dataSourceFactoryCaptor.firstValue)
pageableDataSource.retryFailedRequest()
pageableBaseDataSource.retryFailedRequest()
verify(dataSourceFactory).retryFailedRequest()
}
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<String>>, KArgumentCaptor<SearchDataSourceFactory<String>>> {
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<String>>, KArgumentCaptor<PagingDataSourceFactory<String>>> {
val captor = argumentCaptor<() -> Unit>()
val dataSourceFactoryCaptor = argumentCaptor<SearchDataSourceFactory<String>>()
val dataSourceFactoryCaptor = argumentCaptor<PagingDataSourceFactory<String>>()
val liveData: LiveData<PagedList<String>> = mock()
whenever(liveDataConverter.convert(dataSourceFactoryCaptor.capture(), captor.capture()))
.thenReturn(liveData)

View file

@ -14,21 +14,21 @@ import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
class SearchDataSourceFactoryTest {
class PagingDataSourceFactoryTest {
@Mock
private lateinit var depictsClient: DepictsClient
@Mock
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var factory: SearchDataSourceFactory<String>
private lateinit var factory: PagingDataSourceFactory<String>
private var function: (Int, Int) -> List<String> = mock()
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
factory = object : SearchDataSourceFactory<String>(loadingStates) {
factory = object : PagingDataSourceFactory<String>(loadingStates) {
override val loadFunction get() = function
}
}

View file

@ -139,7 +139,7 @@ class MediaClientTest {
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Single.just(mockResponse))
mediaClient!!.getMedia("abcde").test().assertErrorMessage("empty list passed for ids")
mediaClient!!.getMedia("abcde").test().assertErrorMessage("List is empty.")
}
@Test