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();
}
}
}

View file

@ -9,20 +9,6 @@
android:id="@+id/mediaDetailFrameLayout"
>
<LinearLayout
android:id="@+id/dummy_category_edit_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="35dp"
android:layout_gravity="bottom"
android:visibility="gone"
android:orientation="vertical"
android:weightSum="10">
<include
layout="@layout/layout_edit_categories" />
</LinearLayout>
<LinearLayout
android:id="@+id/dummy_caption_description_container"
android:layout_width="match_parent"
@ -368,14 +354,39 @@
</LinearLayout>
<Button
android:id="@+id/categoryEditButton"
android:layout_width="24dp"
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_margin="@dimen/standard_gap"
android:layout_gravity="end"
android:background="@drawable/ic_baseline_edit_24" />
android:layout_margin="@dimen/standard_gap">
<ProgressBar
android:id="@+id/progressBarEditCategory"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/mediaDetailCategoryContainer"
app:layout_constraintBottom_toTopOf="@id/mediaDetailuploadeddate" />
<Button
android:id="@+id/categoryEditButton"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="end"
android:visibility="gone"
android:background="@drawable/ic_baseline_edit_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/mediaDetailCategoryContainer"
app:layout_constraintBottom_toTopOf="@id/mediaDetailuploadeddate"/>
</androidx.constraintlayout.widget.ConstraintLayout>
<LinearLayout
style="@style/MediaDetailContainer"
android:layout_width="match_parent"

View file

@ -1,119 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/category_edit_layout"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:orientation="vertical"
android:background="?attr/mainBackground"
android:elevation="30dp">
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="@dimen/half_standard_height"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="Type categories"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<FrameLayout
android:id="@+id/category_search_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_container_search"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<SearchView
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:queryHint="@string/categories_search_text_hint"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/pb_categories"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/existing_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<TextView
android:id="@+id/no_results_found"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="No results found"
android:textSize="@dimen/description_text_size"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_categories"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_200"
android:background="?attr/mainBackground"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/cancel_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/opak_middle_grey"
/>
<Button
android:id="@+id/update_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/category_edit_button_text"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/button_blue"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View file

@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/category_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<CheckBox
android:id="@+id/category_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkMark="?android:attr/textCheckMark"
android:checked="false"
android:gravity="center_vertical"
android:padding="@dimen/tiny_gap"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/category_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Category"
app:layout_constraintLeft_toRightOf="@+id/category_checkbox"
app:layout_constraintTop_toTopOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -6,6 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/standard_gap"
android:background="?attr/mainBackground"
>
<LinearLayout
android:layout_width="match_parent"

View file

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* StarrySky
-->
<resources>
<string name="crash_dialog_title">維基共享資源已當機</string>
<string name="crash_dialog_text">哎呀,出錯了!</string>
<string name="crash_dialog_comment_prompt">透過電子郵件告訴我們您先前做了什麼,這將協助我們修復它!</string>
<string name="crash_dialog_ok_toast">謝謝您!</string>
</resources>

View file

@ -1,215 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Liuxinyu970226
* StarrySky
-->
<resources>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">正在上傳%1$d個檔案</item>
<item quantity="other">正在上傳%1$d個檔案</item>
</plurals>
<plurals name="contributions_subtitle">
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">開始上傳</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">正在處理%d個上傳</item>
<item quantity="other">正在處理%d個上傳</item>
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%d個上傳</item>
<item quantity="other">%d個上傳</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">此圖片會按 %1$s 協議授權上傳</item>
<item quantity="other">這些圖片會按 %1$s 協議授權上傳</item>
</plurals>
<plurals name="upload_count_title">
<item quantity="one">%1$d 個上傳</item>
<item quantity="other">%1$d 個上傳</item>
</plurals>
<plurals name="receiving_shared_content">
<item quantity="one">接收分享內容。處理圖片的時間取決於圖片大小與您的裝置</item>
<item quantity="other">接收分享內容。處理圖片的時間取決於圖片大小與您的裝置</item>
</plurals>
<string name="navigation_item_explore">探索</string>
<string name="preference_category_appearance">外觀</string>
<string name="preference_category_general">一般</string>
<string name="preference_category_feedback">意見回饋</string>
<string name="preference_category_privacy">隱私</string>
<string name="app_name">維基共享資源</string>
<string name="menu_settings">設定</string>
<string name="intent_share_upload_label">上傳至維基共享資源</string>
<string name="username">用戶名</string>
<string name="password">密碼</string>
<string name="login">登入</string>
<string name="forgot_password">忘記了密碼?</string>
<string name="signup">註冊</string>
<string name="logging_in_title">登入中</string>
<string name="logging_in_message">請稍候…</string>
<string name="updating_caption_title">正在更新說明與描述</string>
<string name="updating_caption_message">請等待⋯</string>
<string name="login_success">登入成功!</string>
<string name="login_failed">登入失敗!</string>
<string name="upload_failed">找不到檔案。請試試看其它檔案。</string>
<string name="authentication_failed">身份驗證失敗,請重新登入</string>
<string name="uploading_started">開始上傳!</string>
<string name="uploading_queued">上傳已排定(限制連線模式已啟用)</string>
<string name="upload_completed_notification_title">已上傳%1$s</string>
<string name="upload_completed_notification_text">輕觸來檢視您上傳的項目</string>
<string name="upload_progress_notification_title_start">開始上傳檔案:%s</string>
<string name="upload_progress_notification_title_in_progress">正在上傳%1$s</string>
<string name="upload_progress_notification_title_finishing">即將完成上傳%1$s</string>
<string name="upload_failed_notification_title">上傳%1$s失敗</string>
<string name="upload_paused_notification_title">已暫停上傳%1$s</string>
<string name="upload_failed_notification_subtitle">輕觸檢視</string>
<string name="upload_paused_notification_subtitle">輕觸檢視</string>
<string name="title_activity_contributions">我最近的上傳</string>
<string name="contribution_state_queued">佇列</string>
<string name="contribution_state_failed">失敗</string>
<string name="contribution_state_in_progress">%1$d%%完成</string>
<string name="contribution_state_starting">上傳中</string>
<string name="menu_from_gallery">自圖庫</string>
<string name="menu_from_camera">拍照</string>
<string name="menu_nearby">附近</string>
<string name="provider_contributions">我的上傳</string>
<string name="menu_share">分享</string>
<string name="menu_view_file_page">檢視檔案頁面</string>
<string name="share_title_hint">說明(必填)</string>
<string name="add_caption_toast">請提供此檔案的說明</string>
<string name="share_description_hint">描述</string>
<string name="share_caption_hint">說明</string>
<string name="login_failed_network">無法登入-網路故障</string>
<string name="login_failed_throttled">失敗次數過多。請於幾分鐘後重試。</string>
<string name="login_failed_blocked">很抱歉,該使用者已被維基共享資源封鎖</string>
<string name="login_failed_2fa_needed">必須提供您的雙重驗證代碼。</string>
<string name="login_failed_generic">登入失敗!</string>
<string name="share_upload_button">上載</string>
<string name="multiple_share_base_title">命名此組圖像</string>
<string name="provider_modifications">修改</string>
<string name="menu_upload_single">上載</string>
<string name="categories_search_text_hint">搜尋分類</string>
<string name="depicts_search_text_hint">搜尋您的媒體所描寫項目(高山、泰姬瑪、其它等。)</string>
<string name="menu_save_categories">儲存</string>
<string name="refresh_button">刷新</string>
<string name="display_list_button">清單</string>
<string name="contributions_subtitle_zero">(尚未上傳)</string>
<string name="categories_not_found">沒有發現與 %1$s 相符的分類</string>
<string name="depictions_not_found">找不到符合%1$s的維基數據項目</string>
<string name="no_child_classes">%1$s沒有子類別</string>
<string name="no_parent_classes">%1$s沒有父類別</string>
<string name="categories_skip_explanation">為您的圖片添加分類,使別人在維基共享資源更容易找到。\n\n開始輸入以添加分類。</string>
<string name="categories_activity_title">分類:</string>
<string name="title_activity_settings">設定</string>
<string name="title_activity_signup">註冊</string>
<string name="title_activity_featured_images">特色圖片</string>
<string name="title_activity_custom_selector">自訂選擇器</string>
<string name="title_activity_category_details">分類</string>
<string name="title_activity_review">同行評審</string>
<string name="menu_about">關於</string>
<string name="about_license">維基共享資源應用程式是透過維基媒體社群上的受讓人,與志願者們所建立及維護的開源應用程式。維基媒體基金會並不參與此應用程式的建立、開發,與維護。</string>
<string name="about_improve">建立新的&lt;a href=\"%1$s\"&gt; GitHub 問題&lt;/a&gt;來回報程式錯誤和提出建議。</string>
<string name="about_privacy_policy">&lt;u&gt;私隱政策&lt;/u&gt;</string>
<string name="about_credits">製作人員</string>
<string name="title_activity_about">關於</string>
<string name="menu_feedback">發送回饋 (通過電子郵件)</string>
<string name="no_email_client">未安裝電子郵件客戶端</string>
<string name="provider_categories">最近使用過的分類</string>
<string name="waiting_first_sync">等待首次同步…</string>
<string name="no_uploads_yet">您尚未上傳過任何照片。</string>
<string name="menu_retry_upload">重試</string>
<string name="menu_cancel_upload">取消</string>
<string name="media_upload_policy">透過提交此圖片,我宣佈這是我個人創作的成品,且不包含受版權保護或自拍內容,並除此之外遵守&lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;維基媒體共享資源方針&lt;/a&gt;</string>
<string name="menu_download">下載</string>
<string name="preference_license">預設授權條款</string>
<string name="use_previous">使用先前標題及描述</string>
<string name="preference_theme">主題</string>
<string name="license_name_cc_by_sa_four">姓名標示-相同方式分享4.0</string>
<string name="license_name_cc_by_four">姓名標示4.0</string>
<string name="license_name_cc_by_sa"> 姓名標示-相同方式分享3.0</string>
<string name="license_name_cc_by">姓名標示3.0</string>
<string name="tutorial_1_text">維基共享資源管理大部份使用在維基百科的圖片。</string>
<string name="tutorial_1_subtext">您的圖片可以幫助教育世界上的人們!</string>
<string name="tutorial_2_text">請上傳完全由您自己拍攝或創作的圖片:</string>
<string name="tutorial_2_subtext_1">自然事物(花、動物、山峰等)</string>
<string name="tutorial_2_subtext_2">可利用的事物(腳踏車、車站等)</string>
<string name="tutorial_2_subtext_3">知名人物(您的市長、您所見過的奧運選手等)</string>
<string name="tutorial_3_text">請不要上傳:</string>
<string name="tutorial_3_subtext_1">您朋友的自拍和圖片</string>
<string name="tutorial_3_subtext_2">您從網路下載來的圖片</string>
<string name="tutorial_3_subtext_3">專有應用程式的截圖</string>
<string name="tutorial_4_text">上傳範例:</string>
<string name="tutorial_4_subtext_1">標題:雪梨歌劇院</string>
<string name="tutorial_4_subtext_2">描述:從對面海灣所看到的雪梨歌劇院</string>
<string name="tutorial_4_subtext_3">分類Sydney Opera House from the west雪梨歌劇院的西側、Sydney Opera House remote views雪梨歌劇院的遠方外觀</string>
<string name="welcome_wikipedia_text">貢獻您的圖片,使維基百科的文章更加生動!</string>
<string name="welcome_wikipedia_subtext">維基百科上的圖像來自維基共享資源。</string>
<string name="welcome_copyright_text">您的圖片可以幫助教育世界上的人們!</string>
<string name="welcome_copyright_subtext">避免使用受版權保護的材料,例如從網際網路找來的圖片、海報、書籍封面等</string>
<string name="welcome_final_text">以上您明白了嗎?</string>
<string name="welcome_final_button_text">是!</string>
<string name="welcome_help_button_text">更多資訊</string>
<string name="detail_panel_cats_label">分類:</string>
<string name="detail_panel_cats_loading">載入中…</string>
<string name="detail_panel_cats_none">未選擇</string>
<string name="detail_caption_empty">沒有說明</string>
<string name="detail_description_empty">無描述</string>
<string name="detail_discussion_empty">無討論</string>
<string name="detail_license_empty">不明授權</string>
<string name="menu_refresh">刷新</string>
<string name="storage_permission_title">請求儲存裝置權限</string>
<string name="read_storage_permission_rationale">必要權限:讀取外部儲存裝置。否則應用程式無法存取您的圖庫。</string>
<string name="write_storage_permission_rationale">必要權限:寫入外部存儲裝置。否則應用程式無法取用您的相機/圖庫。</string>
<string name="location_permission_title">請求儲存裝置權限</string>
<string name="ok">確定</string>
<string name="warning">警告!</string>
<string name="duplicate_image_found">發現重複圖片</string>
<string name="upload">上載</string>
<string name="yes"></string>
<string name="no"></string>
<string name="media_detail_caption">說明</string>
<string name="media_detail_title">頁面標題</string>
<string name="media_detail_depiction">描述</string>
<string name="media_detail_description">描述</string>
<string name="media_detail_discussion">討論</string>
<string name="media_detail_author">製作者</string>
<string name="media_detail_uploaded_date">上傳日期</string>
<string name="media_detail_license">授權</string>
<string name="media_detail_coordinates">座標</string>
<string name="media_detail_coordinates_empty">未提供</string>
<string name="become_a_tester_title">成為測試人員</string>
<string name="become_a_tester_description">選擇加入我們在 Google Play 上的 beta 測試版本,以提早取用新功能及程式修正</string>
<string name="_2fa_code">雙重驗證代碼</string>
<string name="logout_verification">您確定要登出嗎?</string>
<string name="commons_logo">維基共享資源標誌</string>
<string name="commons_website">共享資源網站</string>
<string name="commons_facebook">維基共享資源臉書頁</string>
<string name="commons_github">Github 上的共享資源原始碼</string>
<string name="mediaimage_failed">媒體圖片失敗</string>
<string name="no_subcategory_found">找不到子分類</string>
<string name="no_parentcategory_found">找不到母分類</string>
<string name="welcome_image_mount_zao">藏王連峰</string>
<string name="welcome_image_llamas">大羊駝</string>
<string name="welcome_image_rainbow_bridge">彩虹橋</string>
<string name="welcome_image_tulip">鬱金香</string>
<string name="welcome_image_welcome_wikipedia">歡迎來到維基百科</string>
<string name="welcome_image_welcome_copyright">歡迎版權</string>
<string name="welcome_image_sydney_opera_house">雪梨歌劇院</string>
<string name="cancel">取消</string>
<string name="navigation_drawer_open">打開</string>
<string name="navigation_drawer_close">關閉</string>
<string name="navigation_item_home">首頁</string>
<string name="navigation_item_upload">上載</string>
<string name="navigation_item_nearby">附近</string>
<string name="navigation_item_about">關於</string>
<string name="navigation_item_settings">設定</string>
<string name="navigation_item_feedback">意見回饋</string>
<string name="navigation_item_logout">登出</string>
<string name="navigation_item_info">教程</string>
<string name="navigation_item_notification">通知</string>
<string name="navigation_item_review">檢閱</string>
<string name="nearby_info_menu_wikidata_article">維基數據項目</string>
<string name="nearby_info_menu_wikipedia_article">維基百科條目</string>
<string name="description_info">請盡可能描述媒體內容:拍攝於何處?是顯示什麼事物?有什麼脈絡?請描述對象或人物。透露出一些較不易猜測的訊息,例如是風景的話,可以是一天裡的時間。如果媒體顯示出了一些不尋常的事物,請說明不尋常原因。</string>
</resources>

View file

@ -691,6 +691,7 @@ Upload your first media by tapping on the add button.</string>
<string name="achievements_of_user">Achievements of User: %s</string>
<string name="menu_view_user_page">View user page</string>
<string name="edit_depictions">Edit depictions</string>
<string name="edit_categories">Edit categories</string>
<string name="advanced_options">Advanced Options</string>
<string name="advanced_query_info_text">You can customize the Nearby query. If you get errors, reset and apply.</string>
<string name="apply">Apply</string>

View file

@ -294,8 +294,7 @@ class BookmarkListRootFragmentUnitTest {
@Throws(Exception::class)
fun `testBackPressed Case NonNull isVisible and backButton clicked`() {
whenever(mediaDetails.isVisible).thenReturn(true)
whenever(mediaDetails.backButtonClicked()).thenReturn(true)
Assert.assertEquals(fragment.backPressed(), true)
Assert.assertEquals(fragment.backPressed(), false)
}
@Test
@ -303,7 +302,6 @@ class BookmarkListRootFragmentUnitTest {
fun `testBackPressed Case NonNull isVisible and backButton not clicked`() {
Whitebox.setInternalState(fragment, "listFragment", mock(BookmarkPicturesFragment::class.java))
whenever(mediaDetails.isVisible).thenReturn(true)
whenever(mediaDetails.backButtonClicked()).thenReturn(false)
whenever(mediaDetails.removedItems).thenReturn(ArrayList(0))
Assert.assertEquals(fragment.backPressed(), false)
}

View file

@ -8,6 +8,7 @@ import depictedItem
import fr.free.nrw.commons.upload.GpsCategoryModel
import io.reactivex.Single
import io.reactivex.subjects.BehaviorSubject
import media
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers
@ -23,10 +24,16 @@ class CategoriesModelTest {
@Mock
internal lateinit var categoryClient: CategoryClient
@Mock
internal lateinit var gpsCategoryModel: GpsCategoryModel
private lateinit var categoriesModel: CategoriesModel
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
categoriesModel = CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
}
// Test Case for verifying that Categories search (MW api calls)
@ -103,4 +110,106 @@ class CategoriesModelTest {
verify(categoryClient).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT)
}
}
@Test
@Throws(Exception::class)
fun testGetCategoriesByName(){
categoriesModel.getCategoriesByName(listOf("Test"))
}
@Test
@Throws(Exception::class)
fun `Test buildCategories when it returns non empty list`(){
whenever(categoryClient.getCategoriesByName("Test",
"Test", CategoriesModel.SEARCH_CATS_LIMIT
)).thenReturn(Single.just(listOf(categoryItem())))
categoriesModel.buildCategories("Test")
}
@Test
@Throws(Exception::class)
fun `Test buildCategories when it returns empty list`(){
whenever(categoryClient.getCategoriesByName("Test",
"Test", CategoriesModel.SEARCH_CATS_LIMIT
)).thenReturn(Single.just(emptyList()))
categoriesModel.buildCategories("Test")
}
@Test
@Throws(Exception::class)
fun testGetSelectedExistingCategories(){
categoriesModel.getSelectedExistingCategories()
}
@Test
@Throws(Exception::class)
fun testSetSelectedExistingCategories(){
categoriesModel.setSelectedExistingCategories(mutableListOf("Test"))
}
@Test
@Throws(Exception::class)
fun `Test onCategoryItemClicked when media is null and item is selected`(){
categoriesModel.onCategoryItemClicked(
CategoryItem(
"name",
"des",
"image",
true
), null)
}
@Test
@Throws(Exception::class)
fun `Test onCategoryItemClicked when media is null and item is not selected`(){
categoriesModel.onCategoryItemClicked(categoryItem(), null)
}
@Test
@Throws(Exception::class)
fun `Test onCategoryItemClicked when media is not null and item is selected and media contains category`(){
categoriesModel.onCategoryItemClicked(
CategoryItem(
"categories",
"des",
"image",
true
), media())
}
@Test
@Throws(Exception::class)
fun `Test onCategoryItemClicked when media is not null and item is selected and media does not contains category`(){
categoriesModel.onCategoryItemClicked(
CategoryItem(
"name",
"des",
"image",
true
), media())
}
@Test
@Throws(Exception::class)
fun `Test onCategoryItemClicked when media is not null and item is not selected and media contains category`(){
categoriesModel.onCategoryItemClicked(
CategoryItem(
"categories",
"des",
"image",
false
), media())
}
@Test
@Throws(Exception::class)
fun `Test onCategoryItemClicked when media is not null and item is not selected and media does not contains category`(){
categoriesModel.onCategoryItemClicked(
CategoryItem(
"name",
"des",
"image",
false
), media())
}
}

View file

@ -0,0 +1,83 @@
package fr.free.nrw.commons.category
import android.content.Context
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.TestCommonsApplication
import fr.free.nrw.commons.actions.PageEditClient
import fr.free.nrw.commons.notification.NotificationHelper
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import io.reactivex.Single
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
import org.robolectric.annotation.LooperMode
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [21], application = TestCommonsApplication::class)
@LooperMode(LooperMode.Mode.PAUSED)
class CategoryEditHelperUnitTests {
private lateinit var context: Context
private lateinit var helper: CategoryEditHelper
@Mock
private lateinit var notificationHelper: NotificationHelper
@Mock
private lateinit var pageEditClient: PageEditClient
@Mock
private lateinit var viewUtilWrapper: ViewUtilWrapper
@Mock
private lateinit var media: Media
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
context = RuntimeEnvironment.application.applicationContext
helper = CategoryEditHelper(notificationHelper, pageEditClient, viewUtilWrapper,
"")
Mockito.`when`(media.filename).thenReturn("File:Example.jpg")
Mockito.`when`(pageEditClient.getCurrentWikiText(ArgumentMatchers.anyString()))
.thenReturn(Single.just(""))
Mockito.`when`(
pageEditClient.edit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString()
)
).thenReturn(Observable.just(true))
}
@Test
@Throws(Exception::class)
fun checkNotNull() {
Assert.assertNotNull(helper)
}
@Test
@Throws(Exception::class)
fun testMakeCategoryEdit() {
helper.makeCategoryEdit(context, media, listOf("Test"), "[[Category:Test]]")
Mockito.verify(viewUtilWrapper, Mockito.times(1)).showShortToast(
context,
context.getString(R.string.category_edit_helper_make_edit_toast)
)
Mockito.verify(pageEditClient, Mockito.times(1)).edit(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString(),
ArgumentMatchers.anyString()
)
}
}

View file

@ -1,88 +0,0 @@
package fr.free.nrw.commons.category
import android.content.Context
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.nearby.Label
import org.junit.Before
import org.junit.Test
import org.junit.jupiter.api.Assertions.assertEquals
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
class CategoryEditSearchRecyclerViewAdapterUnitTest {
private lateinit var adapter: CategoryEditSearchRecyclerViewAdapter
@Mock
private lateinit var context: Context
@Mock
private lateinit var labels: ArrayList<Label>
@Mock
private lateinit var recyclerView: RecyclerView
@Mock
private lateinit var categoryClient: CategoryClient
@Mock
private lateinit var callback: CategoryEditSearchRecyclerViewAdapter.Callback
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
adapter =
CategoryEditSearchRecyclerViewAdapter(
context,
labels,
recyclerView,
categoryClient,
callback
)
}
@Test
fun testAddToCategories() {
val categories = mutableListOf<String>()
Whitebox.setInternalState(adapter, "categories", categories)
val testCategories = listOf("someString")
adapter.addToCategories(testCategories)
assertEquals(categories.size, testCategories.size)
}
@Test
fun testRemoveFromNewCategories() {
val testCategory = "someString"
val newCategories = mutableListOf(testCategory)
val originalSize = newCategories.size
Whitebox.setInternalState(adapter, "newCategories", newCategories)
adapter.removeFromNewCategories(testCategory)
assertEquals(newCategories.size, originalSize - 1)
}
@Test
fun testAddToNewCategories() {
val testCategory = "someString"
val newCategories = mutableListOf<String>()
val originalSize = newCategories.size
Whitebox.setInternalState(adapter, "newCategories", newCategories)
adapter.addToNewCategories(testCategory)
assertEquals(newCategories.size, originalSize + 1)
}
@Test
fun testGetCategories() {
val categories = mutableListOf<String>()
Whitebox.setInternalState(adapter, "categories", categories)
assertEquals(adapter.categories, categories)
}
@Test
fun testGetNewCategories() {
val newCategories = mutableListOf<String>()
Whitebox.setInternalState(adapter, "newCategories", newCategories)
assertEquals(adapter.newCategories, newCategories)
}
}

View file

@ -259,7 +259,6 @@ class ExploreListRootFragmentUnitTest {
@Throws(Exception::class)
fun `testBackPressed_Case null != mediaDetails && mediaDetails_isVisible_backButtonNotClicked`() {
`when`(mediaDetails.isVisible).thenReturn(true)
`when`(mediaDetails.backButtonClicked()).thenReturn(true)
Assert.assertEquals(fragment.backPressed(), true)
}

View file

@ -129,7 +129,6 @@ class WikidataItemDetailsActivityUnitTests {
@Throws(Exception::class)
fun testOnBackPressedCaseReturn() {
`when`(supportFragmentManager.backStackEntryCount).thenReturn(1)
`when`(mediaDetailPagerFragment.backButtonClicked()).thenReturn(true)
activity.onBackPressed()
}

View file

@ -204,7 +204,6 @@ class SearchActivityUnitTests {
`when`(supportFragmentManager.backStackEntryCount).thenReturn(1)
`when`(mediaDetails.isVisible).thenReturn(true)
activity.refreshNominatedMedia(0)
verify(mediaDetails).backButtonClicked()
}
@Test
@ -215,9 +214,7 @@ class SearchActivityUnitTests {
`when`(mFragments.supportFragmentManager).thenReturn(supportFragmentManager)
`when`(supportFragmentManager.backStackEntryCount).thenReturn(1)
`when`(mediaDetails.isVisible).thenReturn(true)
`when`(mediaDetails.backButtonClicked()).thenReturn(true)
activity.refreshNominatedMedia(0)
verify(mediaDetails).backButtonClicked()
}
@Test
@ -228,12 +225,10 @@ class SearchActivityUnitTests {
Whitebox.setInternalState(activity, "mediaDetails", mediaDetails)
`when`(mFragments.supportFragmentManager).thenReturn(supportFragmentManager)
`when`(supportFragmentManager.backStackEntryCount).thenReturn(1)
`when`(mediaDetails.backButtonClicked()).thenReturn(true)
`when`(mediaDetails.isVisible).thenReturn(true)
val method: Method = SearchActivity::class.java.getDeclaredMethod("onResume")
method.isAccessible = true
method.invoke(activity)
verify(mediaDetails).backButtonClicked()
}
@Test

View file

@ -20,9 +20,9 @@ import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.drawee.generic.GenericDraweeHierarchy
import com.facebook.drawee.view.SimpleDraweeView
import com.facebook.soloader.SoLoader
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.LocationPicker.LocationPickerActivity
import org.robolectric.Shadows.shadowOf
import fr.free.nrw.commons.category.CategoryEditSearchRecyclerViewAdapter
import fr.free.nrw.commons.explore.SearchActivity
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng
@ -97,9 +97,6 @@ class MediaDetailFragmentUnitTests {
@Mock
private lateinit var locationManager: LocationServiceManager
@Mock
private lateinit var categoryEditSearchRecyclerViewAdapter: CategoryEditSearchRecyclerViewAdapter
@Mock
private lateinit var savedInstanceState: Bundle
@ -109,9 +106,6 @@ class MediaDetailFragmentUnitTests {
@Mock
private lateinit var media: Media
@Mock
private lateinit var categoryRecyclerView: RecyclerView
@Mock
private lateinit var simpleDraweeView: SimpleDraweeView
@ -182,10 +176,8 @@ class MediaDetailFragmentUnitTests {
scrollView = view.findViewById(R.id.mediaDetailScrollView)
Whitebox.setInternalState(fragment, "scrollView", scrollView)
categoryRecyclerView = view.findViewById(R.id.rv_categories)
progressBarDeletion = view.findViewById(R.id.progressBarDeletion)
delete = view.findViewById(R.id.nominateDeletion)
Whitebox.setInternalState(fragment, "categoryRecyclerView", categoryRecyclerView)
Whitebox.setInternalState(fragment, "media", media)
Whitebox.setInternalState(fragment, "isDeleted", isDeleted)
@ -212,21 +204,16 @@ class MediaDetailFragmentUnitTests {
Whitebox.setInternalState(fragment, "delete", delete)
Whitebox.setInternalState(fragment, "depictionContainer", linearLayout)
Whitebox.setInternalState(fragment, "toDoLayout", linearLayout)
Whitebox.setInternalState(fragment, "dummyCategoryEditContainer", linearLayout)
Whitebox.setInternalState(fragment, "authorLayout", linearLayout)
Whitebox.setInternalState(fragment, "showCaptionAndDescriptionContainer", linearLayout)
Whitebox.setInternalState(fragment, "updateCategoriesButton", button)
Whitebox.setInternalState(fragment, "editDescription", button)
Whitebox.setInternalState(fragment, "depictEditButton", button)
Whitebox.setInternalState(fragment, "categoryEditButton", button)
Whitebox.setInternalState(fragment, "categoryContainer", linearLayout)
Whitebox.setInternalState(fragment, "categorySearchView", searchView)
Whitebox.setInternalState(fragment, "progressBarDeletion", progressBarDeletion)
Whitebox.setInternalState(fragment, "progressBarEditCategory", progressBarDeletion)
Whitebox.setInternalState(fragment, "mediaDiscussion", textView)
Whitebox.setInternalState(fragment, "locationManager", locationManager)
Whitebox.setInternalState(
fragment,
"categoryEditSearchRecyclerViewAdapter",
categoryEditSearchRecyclerViewAdapter
)
`when`(simpleDraweeView.hierarchy).thenReturn(genericDraweeHierarchy)
val map = HashMap<String, String>()
@ -606,16 +593,6 @@ class MediaDetailFragmentUnitTests {
method.invoke(fragment)
}
@Test
@Throws(Exception::class)
fun testSetupToDo() {
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"setupToDo"
)
method.isAccessible = true
method.invoke(fragment)
}
@Test
@Throws(Exception::class)
fun testOnDiscussionLoaded() {
@ -668,4 +645,46 @@ class MediaDetailFragmentUnitTests {
fun testOnDeleteButtonClicked() {
fragment.onDeleteButtonClicked()
}
@Test
@Throws(Exception::class)
fun testOnCategoryEditButtonClicked() {
whenever(media.filename).thenReturn("File:Example.jpg")
fragment.onCategoryEditButtonClicked()
verify(media, times(1)).filename
}
@Test
@Throws(Exception::class)
fun testDisplayMediaDetails() {
whenever(media.filename).thenReturn("File:Example.jpg")
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"displayMediaDetails"
)
method.isAccessible = true
method.invoke(fragment)
verify(media, times(4)).filename
}
@Test
@Throws(Exception::class)
fun testGotoCategoryEditor() {
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"gotoCategoryEditor",
String::class.java
)
method.isAccessible = true
method.invoke(fragment, "[[Category:Test]]")
}
@Test
@Throws(Exception::class)
fun testOnMediaRefreshed() {
val method: Method = MediaDetailFragment::class.java.getDeclaredMethod(
"onMediaRefreshed",
Media::class.java
)
method.isAccessible = true
method.invoke(fragment, media)
}
}

View file

@ -8,10 +8,13 @@ import fr.free.nrw.commons.upload.categories.CategoriesContract
import fr.free.nrw.commons.upload.categories.CategoriesPresenter
import io.reactivex.Observable
import io.reactivex.schedulers.TestScheduler
import media
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.powermock.reflect.Whitebox
import java.lang.reflect.Method
/**
* The class contains unit test cases for CategoriesPresenter
@ -39,6 +42,27 @@ class CategoriesPresenterTest {
categoriesPresenter.onAttachView(view)
}
@Test
@Throws(Exception::class)
fun testOnAttachViewWithMedia() {
categoriesPresenter.onAttachViewWithMedia(view, media())
}
@Test
@Throws(Exception::class)
fun `Test onAttachViewWithMedia when media is not null`() {
Whitebox.setInternalState(categoriesPresenter, "media", media())
whenever(repository.getCategories(repository.selectedExistingCategories))
.thenReturn(Observable.just(mutableListOf(categoryItem())))
whenever(repository.searchAll("mock", emptyList(), repository.selectedDepictions))
.thenReturn(Observable.just(mutableListOf(categoryItem())))
val method: Method = CategoriesPresenter::class.java.getDeclaredMethod(
"searchResults",
String::class.java
)
method.isAccessible = true
method.invoke(categoriesPresenter, "mock") }
/**
* unit test case for method CategoriesPresenter.searchForCategories
*/
@ -122,6 +146,16 @@ class CategoriesPresenterTest {
fun onCategoryItemClickedTest() {
val categoryItem = categoryItem()
categoriesPresenter.onCategoryItemClicked(categoryItem)
verify(repository).onCategoryClicked(categoryItem)
verify(repository).onCategoryClicked(categoryItem, null)
}
@Test
fun testClearPreviousSelection() {
categoriesPresenter.clearPreviousSelection()
}
@Test
fun testUpdateCategories() {
categoriesPresenter.updateCategories(media(), "[[Category:Test]]")
}
}

View file

@ -2,6 +2,7 @@ package fr.free.nrw.commons.upload
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.category.CategoriesModel
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.contributions.Contribution
@ -73,6 +74,9 @@ class UploadRepositoryUnitTest {
@Mock
private lateinit var imageCoordinates: ImageCoordinates
@Mock
private lateinit var media: Media
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@ -142,8 +146,8 @@ class UploadRepositoryUnitTest {
@Test
fun testOnCategoryClicked() {
repository.onCategoryClicked(categoryItem)
verify(categoriesModel).onCategoryItemClicked(categoryItem)
repository.onCategoryClicked(categoryItem, media)
verify(categoriesModel).onCategoryItemClicked(categoryItem, media)
}
@Test
@ -339,4 +343,21 @@ class UploadRepositoryUnitTest {
method.invoke(repository, null)
}
@Test
fun testGetSelectedExistingCategories() {
assertEquals(repository.selectedExistingCategories,
categoriesModel.getSelectedExistingCategories())
}
@Test
fun testSetSelectedExistingCategories() {
assertEquals(repository.setSelectedExistingCategories(listOf("Test")),
categoriesModel.setSelectedExistingCategories(mutableListOf("Test")))
}
@Test
fun testGetCategories() {
assertEquals(repository.getCategories(listOf("Test")),
categoriesModel.getCategoriesByName(mutableListOf("Test")))
}
}

View file

@ -1,10 +1,12 @@
package fr.free.nrw.commons.upload.categories
import android.app.ProgressDialog
import android.content.Context
import android.os.Looper
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
@ -12,6 +14,10 @@ import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.TestAppAdapter
import fr.free.nrw.commons.TestCommonsApplication
@ -53,6 +59,9 @@ class UploadCategoriesFragmentUnitTests {
@Mock
private lateinit var pbCategories: ProgressBar
@Mock
private lateinit var progressDialog: ProgressDialog
@Mock
private lateinit var tilContainerEtSearch: TextInputLayout
@ -74,6 +83,9 @@ class UploadCategoriesFragmentUnitTests {
@Mock
private lateinit var editable: Editable
@Mock
private lateinit var button: Button
@Mock
private lateinit var adapter: UploadCategoryAdapter
@ -83,6 +95,10 @@ class UploadCategoriesFragmentUnitTests {
@Mock
private lateinit var presenter: CategoriesContract.UserActionListener
@Mock
private lateinit var media: Media
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
@ -108,6 +124,10 @@ class UploadCategoriesFragmentUnitTests {
Whitebox.setInternalState(fragment, "tvTitle", tvTitle)
Whitebox.setInternalState(fragment, "tooltip", tooltip)
Whitebox.setInternalState(fragment, "tvSubTitle", tvSubTitle)
Whitebox.setInternalState(fragment, "btnNext", button)
Whitebox.setInternalState(fragment, "btnPrevious", button)
Whitebox.setInternalState(fragment, "progressDialog", progressDialog)
Whitebox.setInternalState(fragment, "wikiText", "[[Category:Test]]")
}
@Test
@ -185,6 +205,51 @@ class UploadCategoriesFragmentUnitTests {
fragment.showNoCategorySelected()
}
@Test
@Throws(Exception::class)
fun testGetExistingCategories() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.existingCategories
}
@Test
@Throws(Exception::class)
fun testGetFragmentContext() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.fragmentContext
}
@Test
@Throws(Exception::class)
fun testGoBackToPreviousScreen() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.goBackToPreviousScreen()
}
@Test
@Throws(Exception::class)
fun testShowProgressDialog() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.showProgressDialog()
verify(progressDialog, times(0)).show()
}
@Test
@Throws(Exception::class)
fun testDismissProgressDialog() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.dismissProgressDialog()
verify(progressDialog, times(1)).dismiss()
}
@Test
@Throws(Exception::class)
fun `Test showNoCategorySelected when media is not null`() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
Whitebox.setInternalState(fragment, "media", media)
fragment.showNoCategorySelected()
}
@Test
@Throws(Exception::class)
fun testOnNextButtonClicked() {
@ -192,6 +257,14 @@ class UploadCategoriesFragmentUnitTests {
fragment.onNextButtonClicked()
}
@Test
@Throws(Exception::class)
fun `Test onNextButtonClicked when media is not null`() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
Whitebox.setInternalState(fragment, "media", media)
fragment.onNextButtonClicked()
}
@Test
@Throws(Exception::class)
fun testOnPreviousButtonClicked() {
@ -256,4 +329,16 @@ class UploadCategoriesFragmentUnitTests {
method.invoke(fragment)
}
@Test
@Throws(Exception::class)
fun `Test init when media is not null`() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
Whitebox.setInternalState(fragment, "media", media)
val method: Method = UploadCategoriesFragment::class.java.getDeclaredMethod(
"init"
)
method.isAccessible = true
method.invoke(fragment)
}
}