mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-30 22:34:02 +01:00 
			
		
		
		
	* #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:
		
							parent
							
								
									4b22583b60
								
							
						
					
					
						commit
						34ab6f581b
					
				
					 45 changed files with 306 additions and 987 deletions
				
			
		|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|     ); | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     } | ||||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  | @ -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> | ||||
| } | ||||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  | @ -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() | ||||
|     } | ||||
| } | ||||
|  | @ -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); | ||||
|     } | ||||
| } | ||||
|  | @ -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 | ||||
|  | @ -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() | ||||
|     } | ||||
| } | ||||
|  | @ -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(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -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 | ||||
|     } | ||||
|  | @ -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) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | @ -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) | ||||
|  | @ -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 | ||||
|     } | ||||
| 
 | ||||
|  | @ -269,9 +269,7 @@ public class SearchActivity extends NavigationBaseActivity | |||
|      */ | ||||
|     @Override | ||||
|     public void requestMoreImages() { | ||||
|         if (searchMediaFragment!=null){ | ||||
|             searchMediaFragment.requestMoreImages(); | ||||
|         } | ||||
|         //unneeded | ||||
|     } | ||||
| 
 | ||||
|     @Override protected void onDestroy() { | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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> | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  |  | |||
|  | @ -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> | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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() | ||||
|     } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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> | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) } | ||||
|                 } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Seán Mac Gillicuddy
						Seán Mac Gillicuddy