Fixes 4620 : Editing categories of an existing picture: Reuse categories selection UI from the Upload Wizard (#4928)

* Entry to new UI

* Getting existing categories

* Hidden categories managed

* Category edit updated

* Category Edition implemented

* Java docs added

* Java docs added

* Java docs added

* Previous UI discarded

* Test added

* More test added

* More test added

* More test added

* More test added

* More java docs added

* Minor changes
This commit is contained in:
Ayan Sarkar 2022-04-11 16:00:21 +05:30 committed by GitHub
parent 48343035d3
commit 11292ab514
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 977 additions and 913 deletions

View file

@ -206,10 +206,6 @@ public class BookmarkListRootFragment extends CommonsDaggerSupportFragment imple
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if (mediaDetails != null) {
if (mediaDetails.isVisible()) {
if (mediaDetails.backButtonClicked()) {
// mediaDetails handled the back clicked , no further action required.
return true;
}
// todo add get list fragment
((BookmarkFragment) getParentFragment()).setupTabLayout();
ArrayList<Integer> removed = mediaDetails.getRemovedItems();

View file

@ -1,11 +1,11 @@
package fr.free.nrw.commons.category
import android.text.TextUtils
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.upload.GpsCategoryModel
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.StringSortingUtils
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.functions.Function4
import timber.log.Timber
import java.util.*
@ -21,6 +21,11 @@ class CategoriesModel @Inject constructor(
) {
private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
/**
* Existing categories which are selected
*/
private var selectedExistingCategories: MutableList<String> = mutableListOf()
/**
* Returns if the item contains an year
* @param item
@ -119,6 +124,41 @@ class CategoriesModel @Inject constructor(
}.toList().toObservable()
}
/**
* Fetches details of every category by their name, converts them into
* CategoryItem and returns them in a list.
*
* @param categoryNames selected Categories
* @return List of CategoryItem
*/
fun getCategoriesByName(categoryNames: List<String>):
Observable<MutableList<CategoryItem>>? {
return Observable.fromIterable(categoryNames)
.map { categoryName ->
buildCategories(categoryName)
}.toList().toObservable()
}
/**
* Fetches the categories and converts them into CategoryItem
*/
fun buildCategories(categoryName: String): CategoryItem {
return categoryClient.getCategoriesByName(categoryName,
categoryName, SEARCH_CATS_LIMIT).map {
if(it.isNotEmpty()) {
CategoryItem(
it[0].name, it[0].description,
it[0].thumbnail, it[0].isSelected
)
} else {
CategoryItem(
"Hidden", "Hidden",
"hidden", false
)
}
}.blockingGet()
}
private fun combine(
depictionCategories: List<CategoryItem>,
locationCategories: List<CategoryItem>,
@ -154,12 +194,35 @@ class CategoriesModel @Inject constructor(
* Handles category item selection
* @param item
*/
fun onCategoryItemClicked(item: CategoryItem) {
if (item.isSelected) {
selectedCategories.add(item)
updateCategoryCount(item)
fun onCategoryItemClicked(item: CategoryItem, media: Media?) {
if (media == null) {
if (item.isSelected) {
selectedCategories.add(item)
updateCategoryCount(item)
} else {
selectedCategories.remove(item)
}
} else {
selectedCategories.remove(item)
if (item.isSelected) {
if (media.categories?.contains(item.name) == true) {
selectedExistingCategories.add(item.name)
} else {
selectedCategories.add(item)
updateCategoryCount(item)
}
} else {
if (media.categories?.contains(item.name) == true) {
selectedExistingCategories.remove(item.name)
if (!media.categories?.contains(item.name)!!) {
val categoriesList: MutableList<String> = ArrayList()
categoriesList.add(item.name)
categoriesList.addAll(media.categories!!)
media.categories = categoriesList
}
} else {
selectedCategories.remove(item)
}
}
}
}
@ -176,9 +239,28 @@ class CategoriesModel @Inject constructor(
*/
fun cleanUp() {
selectedCategories.clear()
selectedExistingCategories.clear()
}
companion object {
const val SEARCH_CATS_LIMIT = 25
}
/**
* Provides selected existing categories
*
* @return selected existing categories
*/
fun getSelectedExistingCategories(): List<String> {
return selectedExistingCategories
}
/**
* Initialize existing categories
*
* @param selectedExistingCategories existing categories
*/
fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) {
this.selectedExistingCategories = selectedExistingCategories
}
}

View file

@ -216,12 +216,6 @@ public class CategoryDetailsActivity extends BaseActivity
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
// the back press is handled by the mediaDetails , no further action required.
if(mediaDetails.backButtonClicked()){
return;
}
tabLayout.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.VISIBLE);
mediaContainer.setVisibility(View.GONE);

View file

@ -24,7 +24,6 @@ public class CategoryEditHelper {
public final PageEditClient pageEditClient;
private final ViewUtilWrapper viewUtil;
private final String username;
private Callback callback;
@Inject
public CategoryEditHelper(NotificationHelper notificationHelper,
@ -44,35 +43,40 @@ public class CategoryEditHelper {
* @param categories
* @return
*/
public Single<Boolean> makeCategoryEdit(Context context, Media media, List<String> categories, Callback callback) {
public Single<Boolean> makeCategoryEdit(Context context, Media media, List<String> categories,
final String wikiText) {
viewUtil.showShortToast(context, context.getString(R.string.category_edit_helper_make_edit_toast));
return addCategory(media, categories)
return addCategory(media, categories, wikiText)
.flatMapSingle(result -> Single.just(showCategoryEditNotification(context, media, result)))
.firstOrError();
}
/**
* Appends new categories
* Rebuilds the WikiText with new categpries and post it on server
*
* @param media
* @param categories to be added
* @return
*/
private Observable<Boolean> addCategory(Media media, List<String> categories) {
private Observable<Boolean> addCategory(Media media, List<String> categories,
final String wikiText) {
Timber.d("thread is category adding %s", Thread.currentThread().getName());
String summary = "Adding categories";
StringBuilder buffer = new StringBuilder();
final StringBuilder buffer = new StringBuilder();
final String wikiTextWithoutCategory
= wikiText.substring(0, wikiText.indexOf("[[Category"));
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
buffer.append("[[Category:").append(categories.get(i)).append("]]\n");
}
} else {
buffer.append("{{subst:unc}}");
}
String appendText = buffer.toString();
return pageEditClient.appendEdit(media.getFilename(), appendText + "\n", summary);
final String appendText = wikiTextWithoutCategory + buffer;
return pageEditClient.edit(media.getFilename(), appendText + "\n", summary);
}
private boolean showCategoryEditNotification(Context context, Media media, boolean result) {

View file

@ -1,162 +0,0 @@
package fr.free.nrw.commons.category;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter.RecyclerViewHolder;
import fr.free.nrw.commons.nearby.Label;
import java.util.ArrayList;
import java.util.List;
public class CategoryEditSearchRecyclerViewAdapter
extends RecyclerView.Adapter<RecyclerViewHolder>
implements Filterable {
private List<String> displayedCategories;
private List<String> categories = new ArrayList<>();
private List<String> newCategories = new ArrayList<>();
private final LayoutInflater inflater;
private CategoryClient categoryClient;
private Context context;
private Callback callback;
public CategoryEditSearchRecyclerViewAdapter(Context context, ArrayList<Label> labels,
RecyclerView categoryRecyclerView, CategoryClient categoryClient, Callback callback) {
this.context = context;
inflater = LayoutInflater.from(context);
this.categoryClient = categoryClient;
this.callback = callback;
}
public void addToCategories(List<String> categories) {
for(String category : categories) {
if (!this.categories.contains(category)) {
this.categories.add(category);
}
}
}
public void removeFromNewCategories(String categoryToBeRemoved) {
if (newCategories.contains(categoryToBeRemoved)) {
newCategories.remove(categoryToBeRemoved);
}
}
public void addToNewCategories(String addedCategory) {
if (!newCategories.contains(addedCategory)) {
newCategories.add(addedCategory);
}
}
public List<String> getCategories() {
return categories;
}
public List<String> getNewCategories() {
return newCategories;
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
FilterResults results = new FilterResults();
List<CategoryItem> resultCategories = categoryClient
.searchCategories(constraint.toString(), 10).blockingGet();
final List<String> namesOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
resultCategories) {
namesOfCommonsCategories.add(category.getName());
}
results.values = namesOfCommonsCategories;
results.count = resultCategories.size();
return results;
}
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
List<String> resultList = (List<String>)results.values;
// Do not re-add already added categories
for (String category : categories) {
if (resultList.contains(category)) {
resultList.remove(category);
}
}
displayedCategories = resultList;
notifyDataSetChanged();
if (displayedCategories.size()==0) {
callback.noResultsFound();
} else {
callback.someResultsFound();
}
}
};
}
public class RecyclerViewHolder extends RecyclerView.ViewHolder {
public CheckBox categoryCheckBox;
public TextView categoryTextView;
public RecyclerViewHolder(View view) {
super(view);
categoryCheckBox = view.findViewById(R.id.category_checkbox);
categoryTextView = view.findViewById(R.id.category_text);
categoryCheckBox.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (isChecked) {
addToNewCategories(categoryTextView.getText().toString());
} else {
removeFromNewCategories(categoryTextView.getText().toString());
}
List<String> allCategories = new ArrayList<>();
allCategories.addAll(categories);
allCategories.addAll(newCategories);
callback.updateSelectedCategoriesTextView(allCategories);
}
});
}
}
@NonNull
@Override
public RecyclerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemView = inflater.inflate(R.layout.layout_edit_category_item , parent, false);
return new RecyclerViewHolder(itemView);
}
@Override
public void onBindViewHolder(@NonNull RecyclerViewHolder holder, int position) {
holder.categoryTextView.setText(displayedCategories.get(position));
}
@Override
public long getItemId(int position) {
return displayedCategories.get(position).hashCode();
}
@Override
public int getItemCount() {
return (displayedCategories == null) ? 0 : displayedCategories.size();
}
public interface Callback {
void updateSelectedCategoriesTextView(List<String> selectedCategories);
void noResultsFound();
void someResultsFound();
}
}

View file

@ -40,7 +40,7 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@ -185,18 +185,12 @@ public class ExploreListRootFragment extends CommonsDaggerSupportFragment implem
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
// todo add get list fragment
if (mediaDetails.backButtonClicked()) {
// MediaDetails handled the event no further action required.
return true;
} else {
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
}
((ExploreFragment) getParentFragment()).tabLayout.setVisibility(View.VISIBLE);
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
} else {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}

View file

@ -259,12 +259,6 @@ public class SearchActivity extends BaseActivity
@Override
public void onBackPressed() {
if (getSupportFragmentManager().getBackStackEntryCount() == 1){
// the back press is handled by the mediaDetails , no further action required.
if(mediaDetails.backButtonClicked()){
return;
}
// back to search so show search toolbar and hide navigation toolbar
searchView.setVisibility(View.VISIBLE);//set the searchview
tabLayout.setVisibility(View.VISIBLE);

View file

@ -183,12 +183,6 @@ public class WikidataItemDetailsActivity extends BaseActivity implements MediaDe
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
// back pressed is handled by the mediaDetails , no further action required.
if(mediaDetailPagerFragment.backButtonClicked()){
return;
}
tabLayout.setVisibility(View.VISIBLE);
viewPager.setVisibility(View.VISIBLE);
mediaContainer.setVisibility(View.GONE);

View file

@ -20,7 +20,6 @@ import android.graphics.drawable.Animatable;
import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
@ -38,15 +37,12 @@ import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.SearchView;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
@ -57,8 +53,6 @@ import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.SimpleDraweeView;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.geometry.LatLng;
import fr.free.nrw.commons.LocationPicker.LocationPicker;
@ -71,8 +65,6 @@ import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.category.CategoryClient;
import fr.free.nrw.commons.category.CategoryDetailsActivity;
import fr.free.nrw.commons.category.CategoryEditHelper;
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter;
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter.Callback;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.coordinates.CoordinateEditHelper;
import fr.free.nrw.commons.delete.DeleteHelper;
@ -83,9 +75,9 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LocationServiceManager;
import fr.free.nrw.commons.nearby.Label;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.utils.ViewUtilWrapper;
@ -99,7 +91,6 @@ import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
@ -107,7 +98,7 @@ import org.wikipedia.language.AppLanguageLookUpTable;
import org.wikipedia.util.DateUtil;
import timber.log.Timber;
public class MediaDetailFragment extends CommonsDaggerSupportFragment implements Callback,
public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
CategoryEditHelper.Callback {
private static final int REQUEST_CODE = 1001 ;
@ -212,24 +203,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
LinearLayout toDoLayout;
@BindView(R.id.toDoReason)
TextView toDoReason;
@BindView(R.id.category_edit_layout)
LinearLayout categoryEditLayout;
@BindView(R.id.et_search)
SearchView categorySearchView;
@BindView(R.id.rv_categories)
RecyclerView categoryRecyclerView;
@BindView(R.id.update_categories_button)
Button updateCategoriesButton;
@BindView(R.id.coordinate_edit)
Button coordinateEditButton;
@BindView(R.id.dummy_category_edit_container)
LinearLayout dummyCategoryEditContainer;
@BindView(R.id.pb_categories)
ProgressBar progressbarCategories;
@BindView(R.id.existing_categories)
TextView existingCategories;
@BindView(R.id.no_results_found)
TextView noResultsFound;
@BindView(R.id.dummy_caption_description_container)
LinearLayout showCaptionAndDescriptionContainer;
@BindView(R.id.show_caption_description_textview)
@ -247,6 +222,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
ProgressBar progressBarDeletion;
@BindView(R.id.progressBarEdit)
ProgressBar progressBarEditDescription;
@BindView(R.id.progressBarEditCategory)
ProgressBar progressBarEditCategory;
@BindView(R.id.description_edit)
Button editDescription;
@ -263,7 +240,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
private int newWidthOfImageView;
private boolean heightVerifyingBoolean = true; // helps in maintaining aspect ratio
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
private CategoryEditSearchRecyclerViewAdapter categoryEditSearchRecyclerViewAdapter;
//Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
private Media media;
@ -396,11 +372,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
.setVisibility(View.GONE);
}
}
categoryEditSearchRecyclerViewAdapter =
new CategoryEditSearchRecyclerViewAdapter(getContext(), new ArrayList<>(
Label.valuesAsList()), categoryRecyclerView, categoryClient, this);
categoryRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
categoryRecyclerView.setAdapter(categoryEditSearchRecyclerViewAdapter);
// detail provider is null when fragment is shown in review activity
if (detailProvider != null) {
media = detailProvider.getMediaAtPosition(index);
@ -473,6 +444,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onMediaRefreshed, Timber::e),
mediaDataExtractor.getCurrentWikiText(
Objects.requireNonNull(media.getFilename()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::updateCategoryList, Timber::e),
mediaDataExtractor.checkDeletionRequestExists(media)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
@ -485,6 +461,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
private void onMediaRefreshed(Media media) {
media.setCategories(this.media.getCategories());
this.media = media;
setTextFields(media);
compositeDisposable.addAll(
@ -494,7 +471,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
.subscribe(this::onDepictionsLoaded, Timber::e)
);
// compositeDisposable.add(disposable);
setupToDo();
}
private void onDiscussionLoaded(String discussion) {
@ -602,30 +578,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
image.setController(controller);
}
/**
* Displays layout about missing actions to inform user
* - Images that they uploaded with no categories/descriptions, so that they can add them
* - Images that can be added to associated Wikipedia articles that have no pictures
*/
private void setupToDo() {
updateToDoWarning();
compositeDisposable.add(RxSearchView.queryTextChanges(categorySearchView)
.takeUntil(RxView.detaches(categorySearchView))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(query -> {
this.categorySearchQuery = query.toString();
//update image list
if (!TextUtils.isEmpty(query)) {
if (categoryEditLayout.getVisibility() == VISIBLE) {
((CategoryEditSearchRecyclerViewAdapter) categoryRecyclerView.getAdapter()).
getFilter().filter(query.toString());
}
}
}, Timber::e
));
}
private void updateToDoWarning() {
String toDoMessage = "";
boolean toDoNeeded = false;
@ -685,11 +637,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
categoryNames.clear();
categoryNames.addAll(media.getCategories());
categoryEditSearchRecyclerViewAdapter.addToCategories(media.getCategories());
updateSelectedCategoriesTextView(categoryEditSearchRecyclerViewAdapter.getCategories());
categoryRecyclerView.setVisibility(GONE);
updateCategoryList();
if (media.getAuthor() == null || media.getAuthor().equals("")) {
authorLayout.setVisibility(GONE);
@ -698,20 +645,35 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
}
private void updateCategoryList() {
List<String> allCategories = new ArrayList<String>( media.getCategories());
if (media.getAddedCategories() != null) {
// TODO this added categories logic should be removed.
// It is just a short term hack. Categories should be fetch everytime they are updated.
// if media.getCategories contains addedCategory, then do not re-add them
for (String addedCategory : media.getAddedCategories()) {
if (allCategories.contains(addedCategory)) {
media.setAddedCategories(null);
break;
}
}
allCategories.addAll(media.getAddedCategories());
/**
* Gets new categories from the WikiText and updates it on the UI
*
* @param s WikiText
*/
private void updateCategoryList(final String s) {
final List<String> allCategories = new ArrayList<String>();
int i = s.indexOf("[[Category:");
while(i != -1){
final String category = s.substring(i+11, s.indexOf("]]", i));
allCategories.add(category);
i = s.indexOf("]]", i);
i = s.indexOf("[[Category:", i);
}
media.setCategories(allCategories);
if (allCategories.isEmpty()) {
// Stick in a filler element.
allCategories.add(getString(R.string.detail_panel_cats_none));
}
categoryEditButton.setVisibility(VISIBLE);
rebuildCatList(allCategories);
}
/**
* Updates the categories
*/
public void updateCategories() {
List<String> allCategories = new ArrayList<String>(media.getAddedCategories());
media.setCategories(allCategories);
if (allCategories.isEmpty()) {
// Stick in a filler element.
allCategories.add(getString(R.string.detail_panel_cats_none));
@ -720,35 +682,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
rebuildCatList(allCategories);
}
@Override
public void updateSelectedCategoriesTextView(List<String> selectedCategories) {
if (selectedCategories == null || selectedCategories.size() == 0) {
updateCategoriesButton.setClickable(false);
updateCategoriesButton.setAlpha(.5f);
} else {
existingCategories.setText(StringUtils.join(selectedCategories,", "));
if (selectedCategories.equals(media.getCategories())) {
updateCategoriesButton.setClickable(false);
updateCategoriesButton.setAlpha(.5f);
} else {
updateCategoriesButton.setClickable(true);
updateCategoriesButton.setAlpha(1f);
}
}
}
@Override
public void noResultsFound() {
categoryRecyclerView.setVisibility(GONE);
noResultsFound.setVisibility(VISIBLE);
}
@Override
public void someResultsFound() {
categoryRecyclerView.setVisibility(VISIBLE);
noResultsFound.setVisibility(GONE);
}
/**
* Populates media details fragment with depiction list
* @param idAndCaptions
@ -802,41 +735,41 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
Toast.makeText(getContext(), getString(R.string.wikicode_copied), Toast.LENGTH_SHORT).show();
}
@OnClick(R.id.dummy_category_edit_container)
public void onOutsideOfCategoryEditClicked() {
if (dummyCategoryEditContainer.getVisibility() == VISIBLE) {
dummyCategoryEditContainer.setVisibility(GONE);
}
}
@OnClick(R.id.categoryEditButton)
public void onCategoryEditButtonClicked(){
displayHideCategorySearch();
progressBarEditCategory.setVisibility(VISIBLE);
categoryEditButton.setVisibility(GONE);
getWikiText();
}
/**
* Hides the categoryEditContainer.
* returns true after closing the categoryEditContainer if open, implying that event was handled.
* else returns false
* @return
* Gets WikiText from the server and send it to catgory editor
*/
public boolean hideCategoryEditContainerIfOpen(){
if (dummyCategoryEditContainer.getVisibility() == VISIBLE) {
// editCategory is open, close it and return true as the event was handled.
dummyCategoryEditContainer.setVisibility(GONE);
return true;
}
// Event was not handled.
return false;
private void getWikiText() {
compositeDisposable.add(mediaDataExtractor.getCurrentWikiText(
Objects.requireNonNull(media.getFilename()))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::gotoCategoryEditor, Timber::e));
}
public void displayHideCategorySearch() {
showCaptionAndDescriptionContainer.setVisibility(GONE);
if (dummyCategoryEditContainer.getVisibility() != VISIBLE) {
dummyCategoryEditContainer.setVisibility(VISIBLE);
} else {
dummyCategoryEditContainer.setVisibility(GONE);
}
/**
* Opens the category editor
*
* @param s WikiText
*/
private void gotoCategoryEditor(final String s) {
categoryEditButton.setVisibility(VISIBLE);
progressBarEditCategory.setVisibility(GONE);
final Fragment categoriesFragment = new UploadCategoriesFragment();
final Bundle bundle = new Bundle();
bundle.putParcelable("Existing_Categories", media);
bundle.putString("WikiText", s);
categoriesFragment.setArguments(bundle);
final FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.mediaDetailFrameLayout, categoriesFragment);
transaction.addToBackStack(null);
transaction.commit();
}
@OnClick(R.id.coordinate_edit)
@ -1113,29 +1046,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
media.setCaptions(updatedCaptions);
}
@OnClick(R.id.update_categories_button)
public void onUpdateCategoriesClicked() {
updateCategories(categoryEditSearchRecyclerViewAdapter.getNewCategories());
displayHideCategorySearch();
}
@OnClick(R.id.cancel_categories_button)
public void onCancelCategoriesClicked() {
displayHideCategorySearch();
}
public void updateCategories(List<String> selectedCategories) {
compositeDisposable.add(categoryEditHelper.makeCategoryEdit(getContext(), media, selectedCategories, this)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(s -> {
Timber.d("Categories are added.");
onOutsideOfCategoryEditClicked();
media.setAddedCategories(selectedCategories);
updateCategoryList();
}));
}
/**
* Fetched coordinates are replaced with existing coordinates by a POST API call.
* @param Latitude to be added
@ -1417,7 +1327,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
@OnClick(R.id.show_caption_description_textview)
void showCaptionAndDescription() {
dummyCategoryEditContainer.setVisibility(GONE);
if (showCaptionAndDescriptionContainer.getVisibility() == GONE) {
showCaptionAndDescriptionContainer.setVisibility(VISIBLE);
setUpCaptionAndDescriptionLayout();

View file

@ -404,16 +404,6 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
public void nominatingForDeletion(int index) {
provider.refreshNominatedMedia(index);
}
/**
* backButtonClicked is called on a back event in the media details pager.
* returns true after closing the categoryEditContainer if open, implying that event was handled.
* else returns false
* @return
*/
public boolean backButtonClicked(){
return ((MediaDetailFragment)(adapter.getCurrentFragment())).hideCategoryEditContainerIfOpen();
}
public interface MediaDetailProvider {
Media getMediaAtPosition(int i);

View file

@ -138,8 +138,8 @@ public class UploadRepository {
*
* @param categoryItem
*/
public void onCategoryClicked(CategoryItem categoryItem) {
categoriesModel.onCategoryItemClicked(categoryItem);
public void onCategoryClicked(CategoryItem categoryItem, final Media media) {
categoriesModel.onCategoryItemClicked(categoryItem, media);
}
/**
@ -354,4 +354,32 @@ public class UploadRepository {
public boolean isWMLSupportedForThisPlace() {
return uploadModel.getItems().get(0).isWLMUpload();
}
/**
* Provides selected existing categories
*
* @return selected existing categories
*/
public List<String> getSelectedExistingCategories() {
return categoriesModel.getSelectedExistingCategories();
}
/**
* Initialize existing categories
*
* @param selectedExistingCategories existing categories
*/
public void setSelectedExistingCategories(final List<String> selectedExistingCategories) {
categoriesModel.setSelectedExistingCategories(selectedExistingCategories);
}
/**
* Takes category names and Gets CategoryItem from the server
*
* @param categories names of Category
* @return Observable<List<CategoryItem>>
*/
public Observable<List<CategoryItem>> getCategories(final List<String> categories){
return categoriesModel.getCategoriesByName(categories);
}
}

View file

@ -1,9 +1,11 @@
package fr.free.nrw.commons.upload.categories;
import java.util.List;
import android.content.Context;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.BasePresenter;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.category.CategoryItem;
import java.util.List;
/**
* The contract with with UploadCategoriesFragment and its presenter would talk to each other
@ -24,6 +26,35 @@ public interface CategoriesContract {
void showNoCategorySelected();
/**
* Gets existing category names from media
*/
List<String> getExistingCategories();
/**
* Returns required context
*/
Context getFragmentContext();
/**
* Returns to previous fragment
*/
void goBackToPreviousScreen();
/**
* Shows the progress dialog
*/
void showProgressDialog();
/**
* Hides the progress dialog
*/
void dismissProgressDialog();
/**
* Refreshes the categories
*/
void refreshCategories();
}
interface UserActionListener extends BasePresenter<View> {
@ -33,6 +64,23 @@ public interface CategoriesContract {
void verifyCategories();
void onCategoryItemClicked(CategoryItem categoryItem);
/**
* Attaches view and media
*/
void onAttachViewWithMedia(@NonNull CategoriesContract.View view, Media media);
/**
* Clears previous selections
*/
void clearPreviousSelection();
/**
* Update the categories
*/
void updateCategories(Media media, String wikiText);
}

View file

@ -1,13 +1,18 @@
package fr.free.nrw.commons.upload.categories
import android.text.TextUtils
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryEditHelper
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.proxy
import io.reactivex.Observable
import io.reactivex.Scheduler
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
import io.reactivex.subjects.PublishSubject
import timber.log.Timber
import javax.inject.Inject
@ -31,6 +36,16 @@ class CategoriesPresenter @Inject constructor(
var view = DUMMY
private val compositeDisposable = CompositeDisposable()
private val searchTerms = PublishSubject.create<String>()
/**
* Current media
*/
private var media: Media? = null
/**
* helper class for editing categories
*/
@Inject
lateinit var categoryEditHelper: CategoryEditHelper
override fun onAttachView(view: CategoriesContract.View) {
this.view = view
@ -59,10 +74,33 @@ class CategoriesPresenter @Inject constructor(
)
}
private fun searchResults(term: String) =
repository.searchAll(term, getImageTitleList(), repository.selectedDepictions)
.subscribeOn(ioScheduler)
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }
/**
* If media is null : Fetches categories from server according to the term
* Else : Fetches existing categories by their name, fetches categories from server according
* to the term and combines both in a list
*/
private fun searchResults(term: String): Observable<List<CategoryItem>>? {
if (media == null) {
return repository.searchAll(term, getImageTitleList(), repository.selectedDepictions)
.subscribeOn(ioScheduler)
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }
} else {
return Observable.zip(
repository.getCategories(repository.selectedExistingCategories)
.map { list -> list.map {
CategoryItem(it.name, it.description, it.thumbnail, true)
}
},
repository.searchAll(term, getImageTitleList(), repository.selectedDepictions),
{ it1, it2 ->
it1 + it2
}
)
.subscribeOn(ioScheduler)
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }
.map { it.filterNot { categoryItem -> categoryItem.thumbnail == "hidden" } }
}
}
override fun onDetachView() {
view = DUMMY
@ -106,6 +144,88 @@ class CategoriesPresenter @Inject constructor(
* @param categoryItem
*/
override fun onCategoryItemClicked(categoryItem: CategoryItem) {
repository.onCategoryClicked(categoryItem)
repository.onCategoryClicked(categoryItem, media)
}
/**
* Attaches view and media
*/
override fun onAttachViewWithMedia(view: CategoriesContract.View, media: Media) {
this.view = view
this.media = media
repository.selectedExistingCategories = view.existingCategories
compositeDisposable.add(
searchTerms
.observeOn(mainThreadScheduler)
.doOnNext {
view.showProgress(true)
view.showError(null)
view.setCategories(null)
}
.switchMap(::searchResults)
.map { repository.selectedCategories + it }
.map { it.distinctBy { categoryItem -> categoryItem.name } }
.observeOn(mainThreadScheduler)
.subscribe(
{
view.setCategories(it)
view.showProgress(false)
if (it.isEmpty()) {
view.showError(R.string.no_categories_found)
}
},
Timber::e
)
)
}
/**
* Clears previous selections
*/
override fun clearPreviousSelection() {
repository.cleanup()
}
/**
* Gets the selected categories and send them for posting to the server
*
* @param media media
* @param wikiText current WikiText from server
*/
override fun updateCategories(media: Media, wikiText: String) {
if (repository.selectedCategories.isNotEmpty()
|| repository.selectedExistingCategories.size != view.existingCategories.size
) {
val selectedCategories: MutableList<String> =
(repository.selectedCategories.map { it.name }.toMutableList()
+ repository.selectedExistingCategories).toMutableList()
if (selectedCategories.isNotEmpty()) {
view.showProgressDialog()
compositeDisposable.add(
categoryEditHelper.makeCategoryEdit(view.fragmentContext, media,
selectedCategories, wikiText)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
Timber.d("Categories are added.")
media.addedCategories = selectedCategories
repository.cleanup()
view.dismissProgressDialog()
view.refreshCategories()
view.goBackToPreviousScreen()
})
{
Timber.e(
"Failed to update categories"
)
}
)
}
} else {
repository.cleanup()
view.showNoCategorySelected()
}
}
}

View file

@ -1,17 +1,23 @@
package fr.free.nrw.commons.upload.categories;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
@ -20,8 +26,11 @@ import butterknife.OnClick;
import com.google.android.material.textfield.TextInputLayout;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxTextView;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.ui.PasteSensitiveTextInputEditText;
import fr.free.nrw.commons.upload.UploadActivity;
import fr.free.nrw.commons.upload.UploadBaseFragment;
@ -29,6 +38,7 @@ import fr.free.nrw.commons.utils.DialogUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import kotlin.Unit;
@ -50,11 +60,27 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
RecyclerView rvCategories;
@BindView(R.id.tooltip)
ImageView tooltip;
@BindView(R.id.btn_next)
Button btnNext;
@BindView(R.id.btn_previous)
Button btnPrevious;
@Inject
CategoriesContract.UserActionListener presenter;
private UploadCategoryAdapter adapter;
private Disposable subscribe;
/**
* Current media
*/
private Media media;
/**
* Progress Dialog for showing background process
*/
private ProgressDialog progressDialog;
/**
* WikiText from the server
*/
private String wikiText;
@Nullable
@Override
@ -67,12 +93,26 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
ButterKnife.bind(this, view);
final Bundle bundle = getArguments();
if (bundle != null) {
media = bundle.getParcelable("Existing_Categories");
wikiText = bundle.getString("WikiText");
}
init();
}
private void init() {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps(), getString(R.string.categories_activity_title)));
if (media == null) {
tvTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
callback.getTotalNumberOfSteps(), getString(R.string.categories_activity_title)));
} else {
tvTitle.setText(R.string.edit_categories);
tvSubTitle.setVisibility(View.GONE);
btnNext.setText(R.string.menu_save_categories);
btnPrevious.setText(R.string.menu_cancel_upload);
}
setTvSubTitle();
tooltip.setOnClickListener(new OnClickListener() {
@Override
@ -80,7 +120,11 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
DialogUtil.showAlertDialog(getActivity(), getString(R.string.categories_activity_title), getString(R.string.categories_tooltip), getString(android.R.string.ok), null, true);
}
});
presenter.onAttachView(this);
if (media == null) {
presenter.onAttachView(this);
} else {
presenter.onAttachViewWithMedia(this, media);
}
initRecyclerView();
addTextChangeListenerToEtSearch();
}
@ -160,24 +204,96 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
@Override
public void showNoCategorySelected() {
DialogUtil.showAlertDialog(getActivity(),
if (media == null) {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.no_categories_selected),
getString(R.string.no_categories_selected_warning_desc),
getString(R.string.continue_message),
getString(R.string.cancel),
() -> goToNextScreen(),
null);
} else {
Toast.makeText(requireContext(), getString(R.string.no_categories_selected),
Toast.LENGTH_SHORT).show();
presenter.clearPreviousSelection();
goBackToPreviousScreen();
}
}
/**
* Gets existing categories from media
*/
@Override
public List<String> getExistingCategories() {
return (media == null) ? null : media.getCategories();
}
/**
* Returns required context
*/
@Override
public Context getFragmentContext() {
return requireContext();
}
/**
* Returns to previous fragment
*/
@Override
public void goBackToPreviousScreen() {
getFragmentManager().popBackStack();
}
/**
* Shows the progress dialog
*/
@Override
public void showProgressDialog() {
progressDialog = new ProgressDialog(requireContext());
progressDialog.setMessage(getString(R.string.please_wait));
progressDialog.show();
}
/**
* Hides the progress dialog
*/
@Override
public void dismissProgressDialog() {
progressDialog.dismiss();
}
/**
* Refreshes the categories
*/
@Override
public void refreshCategories() {
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
assert mediaDetailFragment != null;
mediaDetailFragment.updateCategories();
}
@OnClick(R.id.btn_next)
public void onNextButtonClicked() {
presenter.verifyCategories();
if (media != null) {
presenter.updateCategories(media, wikiText);
} else {
presenter.verifyCategories();
}
}
@OnClick(R.id.btn_previous)
public void onPreviousButtonClicked() {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
if (media != null) {
presenter.clearPreviousSelection();
adapter.setItems(null);
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
assert mediaDetailFragment != null;
mediaDetailFragment.onResume();
goBackToPreviousScreen();
} else {
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
}
}
@Override
@ -188,4 +304,65 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
presenter.searchForCategories(text.toString());
}
}
/**
* Hides the action bar while opening editing fragment
*/
@Override
public void onResume() {
super.onResume();
if (media != null) {
etSearch.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KeyEvent.KEYCODE_BACK) {
etSearch.clearFocus();
presenter.clearPreviousSelection();
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
assert mediaDetailFragment != null;
mediaDetailFragment.onResume();
goBackToPreviousScreen();
return true;
}
return false;
});
Objects.requireNonNull(getView()).setFocusableInTouchMode(true);
getView().requestFocus();
getView().setOnKeyListener((v, keyCode, event) -> {
if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
presenter.clearPreviousSelection();
final MediaDetailFragment mediaDetailFragment = (MediaDetailFragment) getParentFragment();
assert mediaDetailFragment != null;
mediaDetailFragment.onResume();
goBackToPreviousScreen();
return true;
}
return false;
});
Objects.requireNonNull(
((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar())
.hide();
if (getParentFragment().getParentFragment().getParentFragment()
instanceof ContributionsFragment) {
((ContributionsFragment) (getParentFragment()
.getParentFragment().getParentFragment())).nearbyNotificationCardView
.setVisibility(View.GONE);
}
}
}
/**
* Shows the action bar while closing editing fragment
*/
@Override
public void onStop() {
super.onStop();
if (media != null) {
Objects.requireNonNull(
((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar())
.show();
}
}
}