Merge branch 'macgills/3760-categories-pagination' into macgills/3772-images-pagination

# Conflicts:
#	app/src/main/java/fr/free/nrw/commons/Media.java
#	app/src/main/java/fr/free/nrw/commons/media/MediaClient.java
#	app/src/main/java/fr/free/nrw/commons/media/MediaDetailInterface.java
#	app/src/main/java/fr/free/nrw/commons/media/MediaInterface.java
This commit is contained in:
Sean Mac Gillicuddy 2020-05-28 13:26:52 +01:00
commit 350b19e62f
91 changed files with 2281 additions and 1185 deletions

View file

@ -13,7 +13,7 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
## Documentation ## Documentation
We try to have an extensive documentation at [our wiki here at Github][4]: We try to have an extensive documentation at our [documentation repository][4]:
* [User Documentation][5] * [User Documentation][5]
* [Contributor Documentation][6] * [Contributor Documentation][6]
@ -45,11 +45,11 @@ This software is open source, licensed under the [Apache License 2.0][10].
[2]: https://commons-app.github.io/ [2]: https://commons-app.github.io/
[3]: https://github.com/commons-app/apps-android-commons/issues [3]: https://github.com/commons-app/apps-android-commons/issues
[4]: https://github.com/commons-app/apps-android-commons/wiki [4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
[5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation [5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
[6]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation [6]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-contributor-documentation
[7]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21 [7]: https://github.com/commons-app/commons-app-documentation/blob/master/android/Volunteers-welcome!.md#volunteers-welcome
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation [8]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-developer-documentation
[9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used [9]: https://github.com/commons-app/commons-app-documentation/blob/master/android/Libraries-used.md#libraries-used
[10]: https://www.apache.org/licenses/LICENSE-2.0 [10]: https://www.apache.org/licenses/LICENSE-2.0

View file

@ -42,6 +42,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1' implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0' implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION" implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION" implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"

View file

@ -6,6 +6,7 @@ import android.os.Parcelable;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.Depictions; import fr.free.nrw.commons.media.Depictions;
import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.utils.CommonsDateUtil;
@ -15,6 +16,8 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Objects;
import java.util.UUID;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.wikipedia.dataclient.mwapi.MwQueryPage; import org.wikipedia.dataclient.mwapi.MwQueryPage;
import org.wikipedia.gallery.ExtMetadata; import org.wikipedia.gallery.ExtMetadata;
@ -50,6 +53,8 @@ public class Media implements Parcelable {
/** /**
* Wikibase Identifier associated with media files * Wikibase Identifier associated with media files
*/ */
@PrimaryKey
@NonNull
private String pageId; private String pageId;
private List<String> categories; // as loaded at runtime? private List<String> categories; // as loaded at runtime?
/** /**
@ -64,14 +69,28 @@ public class Media implements Parcelable {
* Provides local constructor * Provides local constructor
*/ */
public Media() { public Media() {
pageId = UUID.randomUUID().toString();
} }
public static final Creator<Media> CREATOR = new Creator<Media>() {
@Override
public Media createFromParcel(final Parcel source) {
return new Media(source);
}
@Override
public Media[] newArray(final int size) {
return new Media[size];
}
};
/** /**
* Provides a minimal constructor * Provides a minimal constructor
* *
* @param filename Media filename * @param filename Media filename
*/ */
public Media(String filename) { public Media(final String filename) {
this();
this.filename = filename; this.filename = filename;
} }
@ -86,11 +105,13 @@ public class Media implements Parcelable {
* @param dateUploaded Media date uploaded * @param dateUploaded Media date uploaded
* @param creator Media creator * @param creator Media creator
*/ */
public Media(Uri localUri, String imageUrl, String filename, public Media(final Uri localUri, final String imageUrl, final String filename,
String description, final String description,
long dataLength, Date dateCreated, Date dateUploaded, String creator) { final long dataLength, final Date dateCreated, final Date dateUploaded,
final String creator) {
this();
this.localUri = localUri; this.localUri = localUri;
this.thumbUrl = imageUrl; thumbUrl = imageUrl;
this.imageUrl = imageUrl; this.imageUrl = imageUrl;
this.filename = filename; this.filename = filename;
this.description = description; this.description = description;
@ -100,17 +121,80 @@ public class Media implements Parcelable {
this.creator = creator; this.creator = creator;
} }
public Media(Uri localUri, String filename, /**
String description, String creator, List<String> categories) { * Constructor with all parameters
*/
public Media(final String pageId,
final Uri localUri,
final String thumbUrl,
final String imageUrl,
final String filename,
final String description,
final String discussion,
final long dataLength,
final Date dateCreated,
final Date dateUploaded,
final String license,
final String licenseUrl,
final String creator,
final List<String> categories,
final boolean requestedDeletion,
final LatLng coordinates) {
this.pageId = pageId;
this.localUri = localUri;
this.thumbUrl = thumbUrl;
this.imageUrl = imageUrl;
this.filename = filename;
this.description = description;
this.discussion = discussion;
this.dataLength = dataLength;
this.dateCreated = dateCreated;
this.dateUploaded = dateUploaded;
this.license = license;
this.licenseUrl = licenseUrl;
this.creator = creator;
this.categories = categories;
this.requestedDeletion = requestedDeletion;
this.coordinates = coordinates;
}
public Media(final Uri localUri, final String filename,
final String description, final String creator, final List<String> categories) {
this(localUri,null, filename, this(localUri,null, filename,
description, -1, null, new Date(), creator); description, -1, null, new Date(), creator);
this.categories = categories; this.categories = categories;
} }
public Media(String title, Date date, String user) { public Media(final String title, final Date date, final String user) {
this(null, null, title, "", -1, date, date, user); this(null, null, title, "", -1, date, date, user);
} }
protected Media(final Parcel in) {
localUri = in.readParcelable(Uri.class.getClassLoader());
thumbUrl = in.readString();
imageUrl = in.readString();
filename = in.readString();
thumbnailTitle = in.readString();
caption = in.readString();
description = in.readString();
discussion = in.readString();
dataLength = in.readLong();
final long tmpDateCreated = in.readLong();
dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
final long tmpDateUploaded = in.readLong();
dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
license = in.readString();
licenseUrl = in.readString();
creator = in.readString();
pageId = in.readString();
final ArrayList<String> list = new ArrayList<>();
in.readStringList(list);
categories = list;
in.readParcelable(Depictions.class.getClassLoader());
requestedDeletion = in.readByte() != 0;
coordinates = in.readParcelable(LatLng.class.getClassLoader());
}
/** /**
* Creating Media object from MWQueryPage. * Creating Media object from MWQueryPage.
* Earlier only basic details were set for the media object but going forward, * Earlier only basic details were set for the media object but going forward,
@ -120,14 +204,14 @@ public class Media implements Parcelable {
* @return Media object * @return Media object
*/ */
@NonNull @NonNull
public static Media from(MwQueryPage page) { public static Media from(final MwQueryPage page) {
ImageInfo imageInfo = page.imageInfo(); final ImageInfo imageInfo = page.imageInfo();
if (imageInfo == null) { if (imageInfo == null) {
return new Media(); // null is not allowed return new Media(); // null is not allowed
} }
ExtMetadata metadata = imageInfo.getMetadata(); final ExtMetadata metadata = imageInfo.getMetadata();
if (metadata == null) { if (metadata == null) {
Media media = new Media(null, imageInfo.getOriginalUrl(), final Media media = new Media(null, imageInfo.getOriginalUrl(),
page.title(), "", 0, null, null, null); page.title(), "", 0, null, null, null);
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) { if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
media.setThumbUrl(imageInfo.getThumbUrl()); media.setThumbUrl(imageInfo.getThumbUrl());
@ -135,7 +219,7 @@ public class Media implements Parcelable {
return media; return media;
} }
Media media = new Media(null, final Media media = new Media(null,
imageInfo.getOriginalUrl(), imageInfo.getOriginalUrl(),
page.title(), page.title(),
"", "",
@ -158,11 +242,12 @@ public class Media implements Parcelable {
media.setDescription(metadata.imageDescription()); media.setDescription(metadata.imageDescription());
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories())); media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
String latitude = metadata.getGpsLatitude(); final String latitude = metadata.getGpsLatitude();
String longitude = metadata.getGpsLongitude(); final String longitude = metadata.getGpsLongitude();
if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) { if (!StringUtils.isBlank(latitude) && !StringUtils.isBlank(longitude)) {
LatLng latLng = new LatLng(Double.parseDouble(latitude), Double.parseDouble(longitude), 0); final LatLng latLng = new LatLng(Double.parseDouble(latitude),
Double.parseDouble(longitude), 0);
media.setCoordinates(latLng); media.setCoordinates(latLng);
} }
@ -175,29 +260,17 @@ public class Media implements Parcelable {
* @param metadata * @param metadata
* @return * @return
*/ */
private static String getArtist(ExtMetadata metadata) { private static String getArtist(final ExtMetadata metadata) {
try { try {
String artistHtml = metadata.artist(); final String artistHtml = metadata.artist();
return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">")) return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
.replace("title=\"User:", ""); .replace("title=\"User:", "");
} catch (Exception ex) { } catch (final Exception ex) {
return ""; return "";
} }
} }
/** @Nullable
* @return pageId for the current media object*/
public String getPageId() {
return pageId;
}
/**
*sets pageId for the current media object
*/
public void setPageId(String pageId) {
this.pageId = pageId;
}
public String getThumbUrl() { public String getThumbUrl() {
return thumbUrl; return thumbUrl;
} }
@ -210,11 +283,13 @@ public class Media implements Parcelable {
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : ""; return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
} }
/** @Nullable
* Set Caption(if available) as the thumbnail title of the image private static Date safeParseDate(final String dateStr) {
*/ try {
public void setThumbnailTitle(String title) { return CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr);
this.thumbnailTitle = title; } catch (final ParseException e) {
return null;
}
} }
/** /**
@ -260,19 +335,17 @@ public class Media implements Parcelable {
} }
/** /**
* Sets the name of the file. * @return pageId for the current media object*/
* @param filename the new name of the file @NonNull
*/ public String getPageId() {
public void setFilename(String filename) { return pageId;
this.filename = filename;
} }
/** /**
* Sets the discussion of the file. *sets pageId for the current media object
* @param discussion
*/ */
public void setDiscussion(String discussion) { public void setPageId(final String pageId) {
this.discussion = discussion; this.pageId = pageId;
} }
/** /**
@ -310,11 +383,11 @@ public class Media implements Parcelable {
} }
/** /**
* Sets the file description. * Sets the name of the file.
* @param description the new description of the file * @param filename the new name of the file
*/ */
public void setDescription(String description) { public void setFilename(final String filename) {
this.description = description; this.filename = filename;
} }
/** /**
@ -326,11 +399,11 @@ public class Media implements Parcelable {
} }
/** /**
* Sets the dataLength of the file. * Sets the discussion of the file.
* @param dataLength as a long * @param discussion
*/ */
public void setDataLength(long dataLength) { public void setDiscussion(final String discussion) {
this.dataLength = dataLength; this.discussion = discussion;
} }
/** /**
@ -342,11 +415,11 @@ public class Media implements Parcelable {
} }
/** /**
* Sets the creation date of the file. * Sets the file description.
* @param date creation date as a Date * @param description the new description of the file
*/ */
public void setDateCreated(Date date) { public void setDescription(final String description) {
this.dateCreated = date; this.description = description;
} }
/** /**
@ -368,11 +441,11 @@ public class Media implements Parcelable {
} }
/** /**
* Sets the creator name of the file. * Sets the dataLength of the file.
* @param creator creator name as a string * @param dataLength as a long
*/ */
public void setCreator(String creator) { public void setDataLength(final long dataLength) {
this.creator = creator; this.dataLength = dataLength;
} }
/** /**
@ -383,8 +456,11 @@ public class Media implements Parcelable {
return license; return license;
} }
public void setThumbUrl(String thumbUrl) { /**
this.thumbUrl = thumbUrl; * Set Caption(if available) as the thumbnail title of the image
*/
public void setThumbnailTitle(final String title) {
thumbnailTitle = title;
} }
public String getLicenseUrl() { public String getLicenseUrl() {
@ -392,16 +468,11 @@ public class Media implements Parcelable {
} }
/** /**
* Sets the license name of the file. * Sets the creator name of the file.
* @param license license name as a String * @param creator creator name as a string
*/ */
public void setLicenseInformation(String license, String licenseUrl) { public void setCreator(final String creator) {
this.license = license; this.creator = creator;
if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) {
licenseUrl = "https://" + licenseUrl;
}
this.licenseUrl = licenseUrl;
} }
/** /**
@ -413,12 +484,8 @@ public class Media implements Parcelable {
return coordinates; return coordinates;
} }
/** public void setThumbUrl(final String thumbUrl) {
* Sets the coordinates of where the file was created. this.thumbUrl = thumbUrl;
* @param coordinates file coordinates as a LatLng
*/
public void setCoordinates(@Nullable LatLng coordinates) {
this.coordinates = coordinates;
} }
/** /**
@ -430,6 +497,27 @@ public class Media implements Parcelable {
return categories; return categories;
} }
/**
* Sets the license name of the file.
* @param license license name as a String
*/
public void setLicenseInformation(final String license, String licenseUrl) {
this.license = license;
if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) {
licenseUrl = "https://" + licenseUrl;
}
this.licenseUrl = licenseUrl;
}
/**
* Sets the coordinates of where the file was created.
* @param coordinates file coordinates as a LatLng
*/
public void setCoordinates(@Nullable final LatLng coordinates) {
this.coordinates = coordinates;
}
/** /**
* Sets the categories the file falls under. * Sets the categories the file falls under.
* </p> * </p>
@ -437,26 +525,10 @@ public class Media implements Parcelable {
* and then add the specified ones. * and then add the specified ones.
* @param categories file categories as a list of Strings * @param categories file categories as a list of Strings
*/ */
public void setCategories(List<String> categories) { public void setCategories(final List<String> categories) {
this.categories = categories; this.categories = categories;
} }
@Nullable private static Date safeParseDate(String dateStr) {
try {
return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr);
} catch (ParseException e) {
return null;
}
}
/**
* Set requested deletion to true
* @param requestedDeletion
*/
public void setRequestedDeletion(boolean requestedDeletion){
this.requestedDeletion = requestedDeletion;
}
/** /**
* Get the value of requested deletion * Get the value of requested deletion
* @return boolean requestedDeletion * @return boolean requestedDeletion
@ -465,12 +537,20 @@ public class Media implements Parcelable {
return requestedDeletion; return requestedDeletion;
} }
/**
* Set requested deletion to true
* @param requestedDeletion
*/
public void setRequestedDeletion(final boolean requestedDeletion) {
this.requestedDeletion = requestedDeletion;
}
/** /**
* Sets the license name of the file. * Sets the license name of the file.
* *
* @param license license name as a String * @param license license name as a String
*/ */
public void setLicense(String license) { public void setLicense(final String license) {
this.license = license; this.license = license;
} }
@ -482,15 +562,10 @@ public class Media implements Parcelable {
* This function sets captions * This function sets captions
* @param caption * @param caption
*/ */
public void setCaption(String caption) { public void setCaption(final String caption) {
this.caption = caption; this.caption = caption;
} }
/* Sets depictions for the current media obtained fro Wikibase API*/
public void setDepictions(Depictions depictions) {
this.depictions = depictions;
}
public void setLocalUri(@Nullable final Uri localUri) { public void setLocalUri(@Nullable final Uri localUri) {
this.localUri = localUri; this.localUri = localUri;
} }
@ -516,6 +591,19 @@ public class Media implements Parcelable {
return 0; return 0;
} }
/* Sets depictions for the current media obtained fro Wikibase API*/
public void setDepictions(final Depictions depictions) {
this.depictions = depictions;
}
/**
* Sets the creation date of the file.
* @param date creation date as a Date
*/
public void setDateCreated(final Date date) {
dateCreated = date;
}
/** /**
* Creates a way to transfer information between two or more * Creates a way to transfer information between two or more
* activities. * activities.
@ -523,63 +611,70 @@ public class Media implements Parcelable {
* @param flags Parcel flag * @param flags Parcel flag
*/ */
@Override @Override
public void writeToParcel(Parcel dest, int flags) { public void writeToParcel(final Parcel dest, final int flags) {
dest.writeParcelable(this.localUri, flags); dest.writeParcelable(localUri, flags);
dest.writeString(this.thumbUrl); dest.writeString(thumbUrl);
dest.writeString(this.imageUrl); dest.writeString(imageUrl);
dest.writeString(this.filename); dest.writeString(filename);
dest.writeString(this.thumbnailTitle); dest.writeString(thumbnailTitle);
dest.writeString(this.caption); dest.writeString(caption);
dest.writeString(this.description); dest.writeString(description);
dest.writeString(this.discussion); dest.writeString(discussion);
dest.writeLong(this.dataLength); dest.writeLong(dataLength);
dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1); dest.writeLong(dateCreated != null ? dateCreated.getTime() : -1);
dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1); dest.writeLong(dateUploaded != null ? dateUploaded.getTime() : -1);
dest.writeString(this.license); dest.writeString(license);
dest.writeString(this.licenseUrl); dest.writeString(licenseUrl);
dest.writeString(this.creator); dest.writeString(creator);
dest.writeString(this.pageId); dest.writeString(pageId);
dest.writeStringList(this.categories); dest.writeStringList(categories);
dest.writeParcelable(this.depictions, flags); dest.writeParcelable(depictions, flags);
dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0); dest.writeByte(requestedDeletion ? (byte) 1 : (byte) 0);
dest.writeParcelable(this.coordinates, flags); dest.writeParcelable(coordinates, flags);
} }
protected Media(Parcel in) { /**
this.localUri = in.readParcelable(Uri.class.getClassLoader()); * Equals implementation that matches all parameters for equality check
this.thumbUrl = in.readString(); */
this.imageUrl = in.readString(); @Override
this.filename = in.readString(); public boolean equals(final Object o) {
this.thumbnailTitle = in.readString(); if (this == o) {
this.caption = in.readString(); return true;
this.description = in.readString(); }
this.discussion = in.readString(); if (!(o instanceof Media)) {
this.dataLength = in.readLong(); return false;
long tmpDateCreated = in.readLong(); }
this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated); final Media media = (Media) o;
long tmpDateUploaded = in.readLong(); return getDataLength() == media.getDataLength() &&
this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded); isRequestedDeletion() == media.isRequestedDeletion() &&
this.license = in.readString(); Objects.equals(getLocalUri(), media.getLocalUri()) &&
this.licenseUrl = in.readString(); Objects.equals(getThumbUrl(), media.getThumbUrl()) &&
this.creator = in.readString(); Objects.equals(getImageUrl(), media.getImageUrl()) &&
this.pageId = in.readString(); Objects.equals(getFilename(), media.getFilename()) &&
final ArrayList<String> list = new ArrayList<>(); Objects.equals(getThumbnailTitle(), media.getThumbnailTitle()) &&
in.readStringList(list); Objects.equals(getCaption(), media.getCaption()) &&
this.categories=list; Objects.equals(getDescription(), media.getDescription()) &&
in.readParcelable(Depictions.class.getClassLoader()); Objects.equals(getDiscussion(), media.getDiscussion()) &&
this.requestedDeletion = in.readByte() != 0; Objects.equals(getDateCreated(), media.getDateCreated()) &&
this.coordinates = in.readParcelable(LatLng.class.getClassLoader()); Objects.equals(getDateUploaded(), media.getDateUploaded()) &&
Objects.equals(getLicense(), media.getLicense()) &&
Objects.equals(getLicenseUrl(), media.getLicenseUrl()) &&
Objects.equals(getCreator(), media.getCreator()) &&
getPageId().equals(media.getPageId()) &&
Objects.equals(getCategories(), media.getCategories()) &&
Objects.equals(getDepictions(), media.getDepictions()) &&
Objects.equals(getCoordinates(), media.getCoordinates());
} }
public static final Creator<Media> CREATOR = new Creator<Media>() { /**
@Override * Hashcode implementation that uses all parameters for calculating hash
public Media createFromParcel(Parcel source) { */
return new Media(source); @Override
} public int hashCode() {
return Objects
@Override .hash(getLocalUri(), getThumbUrl(), getImageUrl(), getFilename(), getThumbnailTitle(),
public Media[] newArray(int size) { getCaption(), getDescription(), getDiscussion(), getDataLength(), getDateCreated(),
return new Media[size]; getDateUploaded(), getLicense(), getLicenseUrl(), getCreator(), getPageId(),
} getCategories(), getDepictions(), isRequestedDeletion(), getCoordinates());
}; }
} }

View file

@ -1,11 +1,6 @@
package fr.free.nrw.commons; package fr.free.nrw.commons;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import okhttp3.logging.HttpLoggingInterceptor.Level;
import org.wikipedia.dataclient.SharedPreferenceCookieManager;
import org.wikipedia.dataclient.okhttp.HttpStatusException;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import okhttp3.Cache; import okhttp3.Cache;
@ -26,7 +21,8 @@ public final class OkHttpConnectionFactory {
@NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(), @NonNull private static final Cache NET_CACHE = new Cache(new File(CommonsApplication.getInstance().getCacheDir(),
CACHE_DIR_NAME), NET_CACHE_SIZE); CACHE_DIR_NAME), NET_CACHE_SIZE);
@NonNull private static final OkHttpClient CLIENT = createClient(); @NonNull
private static final OkHttpClient CLIENT = createClient();
@NonNull public static OkHttpClient getClient() { @NonNull public static OkHttpClient getClient() {
return CLIENT; return CLIENT;
@ -45,7 +41,7 @@ public final class OkHttpConnectionFactory {
private static HttpLoggingInterceptor getLoggingInterceptor() { private static HttpLoggingInterceptor getLoggingInterceptor() {
final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor() final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor()
.setLevel(Level.BASIC); .setLevel(Level.BASIC);
httpLoggingInterceptor.redactHeader("Authorization"); httpLoggingInterceptor.redactHeader("Authorization");
httpLoggingInterceptor.redactHeader("Cookie"); httpLoggingInterceptor.redactHeader("Cookie");
@ -54,7 +50,10 @@ public final class OkHttpConnectionFactory {
} }
private static class CommonHeaderRequestInterceptor implements Interceptor { private static class CommonHeaderRequestInterceptor implements Interceptor {
@Override @NonNull public Response intercept(@NonNull final Chain chain) throws IOException {
@Override
@NonNull
public Response intercept(@NonNull final Chain chain) throws IOException {
final Request request = chain.request().newBuilder() final Request request = chain.request().newBuilder()
.header("User-Agent", CommonsApplication.getInstance().getUserAgent()) .header("User-Agent", CommonsApplication.getInstance().getUserAgent())
.build(); .build();
@ -66,16 +65,18 @@ public final class OkHttpConnectionFactory {
private static final String ERRORS_PREFIX = "{\"error"; private static final String ERRORS_PREFIX = "{\"error";
@Override @NonNull public Response intercept(@NonNull final Chain chain) throws IOException { @Override
@NonNull
public Response intercept(@NonNull final Chain chain) throws IOException {
final Response rsp = chain.proceed(chain.request()); final Response rsp = chain.proceed(chain.request());
if (rsp.isSuccessful()) { if (rsp.isSuccessful()) {
try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) { try (final ResponseBody responseBody = rsp.peekBody(ERRORS_PREFIX.length())) {
if (ERRORS_PREFIX.equals(responseBody.string())){ if (ERRORS_PREFIX.equals(responseBody.string())) {
try (final ResponseBody body = rsp.body()) { try (final ResponseBody body = rsp.body()) {
throw new IOException(body.string()); throw new IOException(body.string());
} }
} }
}catch (final IOException e){ } catch (final IOException e) {
Timber.e(e); Timber.e(e);
} }
return rsp; return rsp;

View file

@ -9,6 +9,7 @@ import android.os.RemoteException;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import fr.free.nrw.commons.nearby.NearbyController;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -74,8 +75,10 @@ public class BookmarkLocationsDao {
boolean bookmarkExists = findBookmarkLocation(bookmarkLocation); boolean bookmarkExists = findBookmarkLocation(bookmarkLocation);
if (bookmarkExists) { if (bookmarkExists) {
deleteBookmarkLocation(bookmarkLocation); deleteBookmarkLocation(bookmarkLocation);
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false);
} else { } else {
addBookmarkLocation(bookmarkLocation); addBookmarkLocation(bookmarkLocation);
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true);
} }
return !bookmarkExists; return !bookmarkExists;
} }
@ -160,10 +163,9 @@ public class BookmarkLocationsDao {
location, location,
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)), cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)),
builder.build(), builder.build(),
null, cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)),
null cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESTROYED))
); );
// TODO: add pic and destroyed to bookmark location dao
} }
private ContentValues toContentValues(Place bookmarkLocation) { private ContentValues toContentValues(Place bookmarkLocation) {
@ -179,6 +181,7 @@ public class BookmarkLocationsDao {
cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude()); cv.put(BookmarkLocationsDao.Table.COLUMN_LAT, bookmarkLocation.location.getLatitude());
cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude()); cv.put(BookmarkLocationsDao.Table.COLUMN_LONG, bookmarkLocation.location.getLongitude());
cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic); cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic);
cv.put(BookmarkLocationsDao.Table.COLUMN_DESTROYED, bookmarkLocation.destroyed);
return cv; return cv;
} }
@ -197,6 +200,7 @@ public class BookmarkLocationsDao {
static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link"; static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link";
static final String COLUMN_COMMONS_LINK = "location_commons_link"; static final String COLUMN_COMMONS_LINK = "location_commons_link";
static final String COLUMN_PIC = "location_pic"; static final String COLUMN_PIC = "location_pic";
static final String COLUMN_DESTROYED = "location_destroyed";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = { public static final String[] ALL_FIELDS = {
@ -211,7 +215,8 @@ public class BookmarkLocationsDao {
COLUMN_WIKIPEDIA_LINK, COLUMN_WIKIPEDIA_LINK,
COLUMN_WIKIDATA_LINK, COLUMN_WIKIDATA_LINK,
COLUMN_COMMONS_LINK, COLUMN_COMMONS_LINK,
COLUMN_PIC COLUMN_PIC,
COLUMN_DESTROYED
}; };
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
@ -228,7 +233,8 @@ public class BookmarkLocationsDao {
+ COLUMN_WIKIPEDIA_LINK + " STRING," + COLUMN_WIKIPEDIA_LINK + " STRING,"
+ COLUMN_WIKIDATA_LINK + " STRING," + COLUMN_WIKIDATA_LINK + " STRING,"
+ COLUMN_COMMONS_LINK + " STRING," + COLUMN_COMMONS_LINK + " STRING,"
+ COLUMN_PIC + " STRING" + COLUMN_PIC + " STRING,"
+ COLUMN_DESTROYED + " STRING"
+ ");"; + ");";
public static void onCreate(SQLiteDatabase db) { public static void onCreate(SQLiteDatabase db) {
@ -242,37 +248,24 @@ public class BookmarkLocationsDao {
public static void onUpdate(SQLiteDatabase db, int from, int to) { public static void onUpdate(SQLiteDatabase db, int from, int to) {
Timber.d("bookmarksLocations db is updated from:"+from+", to:"+to); Timber.d("bookmarksLocations db is updated from:"+from+", to:"+to);
if (from == to) { switch (from) {
return; case 7: onCreate(db);
} case 8: // No change
if (from < 7) { case 9: // No change
// doesn't exist yet case 10:
from++; try {
onUpdate(db, from, to); db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;");
return; } catch (SQLiteException exception){
} Timber.e(exception);
if (from == 7) { }
// table added in version 8 case 11: // No change
onCreate(db); case 12:
from++; try {
onUpdate(db, from, to); db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;");
return; }catch (SQLiteException exception){
} Timber.e(exception);
if (from == 8) { }
from++; break;
onUpdate(db, from, to);
return;
}
if (from == 10 && to == 11) {
from++;
//This is safe, and can be called clean, as we/I do not remember the appropriate version for this
//We are anyways switching to room, these things won't be nescessary then
try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;");
}catch (SQLiteException exception){
Timber.e(exception);//
}
return;
} }
} }
} }

View file

@ -2,9 +2,10 @@ package fr.free.nrw.commons.category
import android.text.TextUtils import android.text.TextUtils
import fr.free.nrw.commons.upload.GpsCategoryModel import fr.free.nrw.commons.upload.GpsCategoryModel
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.StringSortingUtils import fr.free.nrw.commons.utils.StringSortingUtils
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.Function3 import io.reactivex.functions.Function4
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -67,30 +68,42 @@ class CategoriesModel @Inject constructor(
* @param imageTitleList * @param imageTitleList
* @return * @return
*/ */
fun searchAll(term: String, imageTitleList: List<String>): Observable<List<CategoryItem>> { fun searchAll(
return suggestionsOrSearch(term, imageTitleList) term: String,
imageTitleList: List<String>,
selectedDepictions: List<DepictedItem>
): Observable<List<CategoryItem>> {
return suggestionsOrSearch(term, imageTitleList, selectedDepictions)
.map { it.map { CategoryItem(it, false) } } .map { it.map { CategoryItem(it, false) } }
} }
private fun suggestionsOrSearch(term: String, imageTitleList: List<String>): private fun suggestionsOrSearch(
Observable<List<String>> { term: String,
imageTitleList: List<String>,
selectedDepictions: List<DepictedItem>
): Observable<List<String>> {
return if (TextUtils.isEmpty(term)) return if (TextUtils.isEmpty(term))
Observable.combineLatest( Observable.combineLatest(
categoriesFromDepiction(selectedDepictions),
gpsCategoryModel.categoriesFromLocation, gpsCategoryModel.categoriesFromLocation,
titleCategories(imageTitleList), titleCategories(imageTitleList),
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)), Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
Function3(::combine) Function4(::combine)
) )
else else
categoryClient.searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT) categoryClient.searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT)
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) } .map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
} }
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>) =
Observable.just(selectedDepictions.map { it.commonsCategories }.flatten())
private fun combine( private fun combine(
depictionCategories: List<String>,
locationCategories: List<String>, locationCategories: List<String>,
titles: List<String>, titles: List<String>,
recents: List<String> recents: List<String>
) = locationCategories + titles + recents ) = depictionCategories + locationCategories + titles + recents
/** /**
@ -98,14 +111,13 @@ class CategoriesModel @Inject constructor(
* @param titleList * @param titleList
* @return * @return
*/ */
private fun titleCategories(titleList: List<String>): Observable<List<String>> { private fun titleCategories(titleList: List<String>) =
return if (titleList.isNotEmpty()) if (titleList.isNotEmpty())
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults -> Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
searchResults.map { it as List<String> }.flatten() searchResults.map { it as List<String> }.flatten()
} }
else else
Observable.just(emptyList()) Observable.just(emptyList())
}
/** /**
* Return category for single title * Return category for single title

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions;
import android.os.Parcel; import android.os.Parcel;
import androidx.room.Entity; import androidx.room.Entity;
import androidx.room.PrimaryKey;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.upload.UploadMediaDetail; import fr.free.nrw.commons.upload.UploadMediaDetail;
@ -13,7 +12,7 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryLogEvent; import java.util.Objects;
@Entity(tableName = "contribution") @Entity(tableName = "contribution")
public class Contribution extends Media { public class Contribution extends Media {
@ -24,24 +23,21 @@ public class Contribution extends Media {
public static final int STATE_QUEUED = 2; public static final int STATE_QUEUED = 2;
public static final int STATE_IN_PROGRESS = 3; public static final int STATE_IN_PROGRESS = 3;
@PrimaryKey (autoGenerate = true)
private long _id;
private int state; private int state;
private long transferred; private long transferred;
private String decimalCoords; private String decimalCoords;
private String dateCreatedSource; private String dateCreatedSource;
private WikidataPlace wikidataPlace; private WikidataPlace wikidataPlace;
/** /**
* Each depiction loaded in depictions activity is associated with a wikidata entity id, * Each depiction loaded in depictions activity is associated with a wikidata entity id, this Id
* this Id is in turn used to upload depictions to wikibase * is in turn used to upload depictions to wikibase
*/ */
private List<DepictedItem> depictedItems = new ArrayList<>(); private List<DepictedItem> depictedItems = new ArrayList<>();
private String mimeType; private String mimeType;
/** /**
* This hasmap stores the list of multilingual captions, where * This hasmap stores the list of multilingual captions, where key of the HashMap is the language
* key of the HashMap is the language and value is the caption in the corresponding language * and value is the caption in the corresponding language Ex: key = "en", value: "<caption in
* Ex: key = "en", value: "<caption in short in English>" * short in English>" key = "de" , value: "<caption in german>"
* key = "de" , value: "<caption in german>"
*/ */
private Map<String, String> captions = new HashMap<>(); private Map<String, String> captions = new HashMap<>();
@ -55,20 +51,13 @@ public class Contribution extends Media {
UploadMediaDetail.formatList(item.getUploadMediaDetails()), UploadMediaDetail.formatList(item.getUploadMediaDetails()),
sessionManager.getAuthorName(), sessionManager.getAuthorName(),
categories); categories);
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails()); captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
decimalCoords = item.getGpsCoords().getDecimalCoords(); decimalCoords = item.getGpsCoords().getDecimalCoords();
dateCreatedSource = ""; dateCreatedSource = "";
this.depictedItems = depictedItems; this.depictedItems = depictedItems;
wikidataPlace = WikidataPlace.from(item.getPlace()); wikidataPlace = WikidataPlace.from(item.getPlace());
} }
public Contribution(final MwQueryLogEvent queryLogEvent, final String user) {
super(queryLogEvent.title(),queryLogEvent.date(), user);
decimalCoords = "";
dateCreatedSource = "";
state = STATE_COMPLETED;
}
public void setDateCreatedSource(final String dateCreatedSource) { public void setDateCreatedSource(final String dateCreatedSource) {
this.dateCreatedSource = dateCreatedSource; this.dateCreatedSource = dateCreatedSource;
} }
@ -108,14 +97,6 @@ public class Contribution extends Media {
return wikidataPlace; return wikidataPlace;
} }
public long get_id() {
return _id;
}
public void set_id(final long _id) {
this._id = _id;
}
public String getDecimalCoords() { public String getDecimalCoords() {
return decimalCoords; return decimalCoords;
} }
@ -128,29 +109,30 @@ public class Contribution extends Media {
this.depictedItems = depictedItems; this.depictedItems = depictedItems;
} }
public void setMimeType(String mimeType) { public String getMimeType() {
this.mimeType = mimeType; return mimeType;
} }
public String getMimeType() { public void setMimeType(final String mimeType) {
return mimeType; this.mimeType = mimeType;
} }
/** /**
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files * Captions are a feature part of Structured data. They are meant to store short, multilingual
* This is a replacement of the previously used titles for images (titles were not multilingual) * descriptions about files This is a replacement of the previously used titles for images (titles
* Also now captions replace the previous convention of using title for filename * were not multilingual) Also now captions replace the previous convention of using title for
* * filename
* <p>
* key of the HashMap is the language and value is the caption in the corresponding language * key of the HashMap is the language and value is the caption in the corresponding language
* * <p>
* returns list of captions stored in hashmap * returns list of captions stored in hashmap
*/ */
public Map<String, String> getCaptions() { public Map<String, String> getCaptions() {
return captions; return captions;
} }
public void setCaptions(Map<String, String> captions) { public void setCaptions(Map<String, String> captions) {
this.captions = captions; this.captions = captions;
} }
@Override @Override
@ -161,7 +143,6 @@ public class Contribution extends Media {
@Override @Override
public void writeToParcel(final Parcel dest, final int flags) { public void writeToParcel(final Parcel dest, final int flags) {
super.writeToParcel(dest, flags); super.writeToParcel(dest, flags);
dest.writeLong(_id);
dest.writeInt(state); dest.writeInt(state);
dest.writeLong(transferred); dest.writeLong(transferred);
dest.writeString(decimalCoords); dest.writeString(decimalCoords);
@ -169,9 +150,24 @@ public class Contribution extends Media {
dest.writeSerializable((HashMap) captions); dest.writeSerializable((HashMap) captions);
} }
/**
* Constructor that takes Media object and state as parameters and builds a new Contribution object
* @param media
* @param state
*/
public Contribution(Media media, int state) {
super(media.getPageId(),
media.getLocalUri(), media.getThumbUrl(), media.getImageUrl(), media.getFilename(),
media.getDescription(),
media.getDiscussion(),
media.getDataLength(), media.getDateCreated(), media.getDateUploaded(),
media.getLicense(), media.getLicenseUrl(), media.getCreator(), media.getCategories(),
media.isRequestedDeletion(), media.getCoordinates());
this.state = state;
}
protected Contribution(final Parcel in) { protected Contribution(final Parcel in) {
super(in); super(in);
_id = in.readLong();
state = in.readInt(); state = in.readInt();
transferred = in.readLong(); transferred = in.readLong();
decimalCoords = in.readString(); decimalCoords = in.readString();
@ -190,4 +186,35 @@ public class Contribution extends Media {
return new Contribution[size]; return new Contribution[size];
} }
}; };
/**
* Equals implementation of Contributions that compares all parameters for checking equality
*/
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Contribution)) {
return false;
}
final Contribution that = (Contribution) o;
return getState() == that.getState() && getTransferred() == that.getTransferred() && Objects
.equals(getDecimalCoords(), that.getDecimalCoords()) && Objects
.equals(getDateCreatedSource(), that.getDateCreatedSource()) && Objects
.equals(getWikidataPlace(), that.getWikidataPlace()) && Objects
.equals(getDepictedItems(), that.getDepictedItems()) && Objects
.equals(getMimeType(), that.getMimeType()) && Objects
.equals(getCaptions(), that.getCaptions());
}
/**
* Hash code implementation of contributions that considers all parameters for calculating hash.
*/
@Override
public int hashCode() {
return Objects
.hash(getState(), getTransferred(), getDecimalCoords(), getDateCreatedSource(),
getWikidataPlace(), getDepictedItems(), getMimeType(), getCaptions());
}
} }

View file

@ -0,0 +1,89 @@
package fr.free.nrw.commons.contributions
import androidx.paging.PagedList.BoundaryCallback
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.di.CommonsApplicationModule
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Scheduler
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.*
import javax.inject.Inject
import javax.inject.Named
/**
* Class that extends PagedList.BoundaryCallback for contributions list It defines the action that
* is triggered for various boundary conditions in the list
*/
class ContributionBoundaryCallback @Inject constructor(
private val repository: ContributionsRepository,
private val sessionManager: SessionManager,
private val mediaClient: MediaClient,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler
) : BoundaryCallback<Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable()
/**
* It is triggered when the list has no items User's Contributions are then fetched from the
* network
*/
override fun onZeroItemsLoaded() {
fetchContributions()
}
/**
* It is triggered when the user scrolls to the top of the list User's Contributions are then
* fetched from the network
* */
override fun onItemAtFrontLoaded(itemAtFront: Contribution) {
fetchContributions()
}
/**
* It is triggered when the user scrolls to the end of the list. User's Contributions are then
* fetched from the network
*/
override fun onItemAtEndLoaded(itemAtEnd: Contribution) {
fetchContributions()
}
/**
* Fetches contributions using the MediaWiki API
*/
fun fetchContributions() {
if (mediaClient.doesMediaListForUserHaveMorePages(sessionManager.userName).not()) {
return
}
compositeDisposable.add(
mediaClient.getMediaListForUser(sessionManager.userName)
.map { mediaList: List<Media?> ->
mediaList.map {
Contribution(it, Contribution.STATE_COMPLETED)
}
}
.subscribeOn(ioThreadScheduler)
.subscribe(
::saveContributionsToDB
) { error: Throwable ->
Timber.e(
"Failed to fetch contributions: %s",
error.message
)
}
)
}
/**
* Saves the contributions the the local DB
*/
private fun saveContributionsToDB(contributions: List<Contribution>) {
compositeDisposable.add(
repository.save(contributions)
.subscribeOn(ioThreadScheduler)
.subscribe { longs: List<Long?>? ->
repository["last_fetch_timestamp"] = System.currentTimeMillis()
}
)
}
}

View file

@ -1,6 +1,6 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData; import androidx.paging.DataSource;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Delete; import androidx.room.Delete;
import androidx.room.Insert; import androidx.room.Insert;
@ -15,40 +15,55 @@ import java.util.List;
@Dao @Dao
public abstract class ContributionDao { public abstract class ContributionDao {
@Query("SELECT * FROM contribution order by dateUploaded DESC") @Query("SELECT * FROM contribution order by dateUploaded DESC")
abstract LiveData<List<Contribution>> fetchContributions(); abstract DataSource.Factory<Integer, Contribution> fetchContributions();
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<Long> save(Contribution contribution); public abstract void saveSynchronous(Contribution contribution);
public Completable deleteAllAndSave(List<Contribution> contributions){ public Completable save(final Contribution contribution) {
return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions)); return Completable
} .fromAction(() -> saveSynchronous(contribution));
}
@Transaction @Transaction
public void deleteAllAndSaveTransaction(List<Contribution> contributions){ public void deleteAndSaveContribution(final Contribution oldContribution,
deleteAll(Contribution.STATE_COMPLETED); final Contribution newContribution) {
save(contributions); deleteSynchronous(oldContribution);
} saveSynchronous(newContribution);
}
@Insert public Completable saveAndDelete(final Contribution oldContribution,
public abstract void save(List<Contribution> contribution); final Contribution newContribution) {
return Completable
.fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution));
}
@Delete @Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<Integer> delete(Contribution contribution); public abstract Single<List<Long>> save(List<Contribution> contribution);
@Query("SELECT * from contribution WHERE filename=:fileName") @Delete
public abstract List<Contribution> getContributionWithTitle(String fileName); public abstract void deleteSynchronous(Contribution contribution);
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)") public Completable delete(final Contribution contribution) {
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates); return Completable
.fromAction(() -> deleteSynchronous(contribution));
}
@Query("Delete FROM contribution") @Query("SELECT * from contribution WHERE filename=:fileName")
public abstract void deleteAll(); public abstract List<Contribution> getContributionWithTitle(String fileName);
@Query("Delete FROM contribution WHERE state = :state") @Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
public abstract void deleteAll(int state); public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
@Update @Query("Delete FROM contribution")
public abstract Single<Integer> update(Contribution contribution); public abstract void deleteAll();
@Update
public abstract void updateSynchronous(Contribution contribution);
public Completable update(final Contribution contribution) {
return Completable
.fromAction(() -> updateSynchronous(contribution));
}
} }

View file

@ -22,7 +22,6 @@ import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.Random;
import timber.log.Timber; import timber.log.Timber;
public class ContributionViewHolder extends RecyclerView.ViewHolder { public class ContributionViewHolder extends RecyclerView.ViewHolder {
@ -39,23 +38,22 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
private int position; private int position;
private Contribution contribution; private Contribution contribution;
private Random random = new Random(); private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient; private final MediaClient mediaClient;
ContributionViewHolder(View parent, Callback callback, ContributionViewHolder(final View parent, final Callback callback,
MediaClient mediaClient) { final MediaClient mediaClient) {
super(parent); super(parent);
this.mediaClient = mediaClient; this.mediaClient = mediaClient;
ButterKnife.bind(this, parent); ButterKnife.bind(this, parent);
this.callback=callback; this.callback=callback;
} }
public void init(int position, Contribution contribution) { public void init(final int position, final Contribution contribution) {
this.contribution = contribution; this.contribution = contribution;
fetchAndDisplayCaption(contribution); fetchAndDisplayCaption(contribution);
this.position = position; this.position = position;
String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri()); final String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) { if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest = final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource)) ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
@ -84,8 +82,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
stateView.setVisibility(View.GONE); stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE); progressView.setVisibility(View.VISIBLE);
failedImageOptions.setVisibility(View.GONE); failedImageOptions.setVisibility(View.GONE);
long total = contribution.getDataLength(); final long total = contribution.getDataLength();
long transferred = contribution.getTransferred(); final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) { if (transferred == 0 || transferred >= total) {
progressView.setIndeterminate(true); progressView.setIndeterminate(true);
} else { } else {
@ -107,14 +105,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
* *
* @param contribution * @param contribution
*/ */
private void fetchAndDisplayCaption(Contribution contribution) { private void fetchAndDisplayCaption(final Contribution contribution) {
if ((contribution.getState() != Contribution.STATE_COMPLETED)) { if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
titleView.setText(contribution.getDisplayTitle()); titleView.setText(contribution.getDisplayTitle());
} else { } else {
final String pageId = contribution.getPageId(); final String pageId = contribution.getPageId();
if (pageId != null) { if (pageId != null) {
Timber.d("Fetching caption for %s", contribution.getFilename()); Timber.d("Fetching caption for %s", contribution.getFilename());
String wikibaseMediaId = PAGE_ID_PREFIX final String wikibaseMediaId = PAGE_ID_PREFIX
+ pageId; // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155 + pageId; // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId) compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -141,7 +139,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
* @return * @return
*/ */
@Nullable @Nullable
private String chooseImageSource(String thumbUrl, Uri localUri) { private String chooseImageSource(final String thumbUrl, final Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl : return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() : localUri != null ? localUri.toString() :
null; null;

View file

@ -12,16 +12,6 @@ public class ContributionsContract {
public interface View { public interface View {
void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow);
void setUploadCount(int count);
void showContributions(List<Contribution> contributionList);
void showMessage(String localizedMessage); void showMessage(String localizedMessage);
} }
@ -31,8 +21,6 @@ public class ContributionsContract {
void deleteUpload(Contribution contribution); void deleteUpload(Contribution contribution);
Media getItemAtPosition(int i);
void updateContribution(Contribution contribution); void updateContribution(Contribution contribution);
void fetchMediaDetails(Contribution contribution); void fetchMediaDetails(Contribution contribution);

View file

@ -19,7 +19,6 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
import androidx.fragment.app.FragmentTransaction; import androidx.fragment.app.FragmentTransaction;
import butterknife.BindView; import butterknife.BindView;
@ -30,8 +29,7 @@ import fr.free.nrw.commons.campaigns.Campaign;
import fr.free.nrw.commons.campaigns.CampaignView; import fr.free.nrw.commons.campaigns.CampaignView;
import fr.free.nrw.commons.campaigns.CampaignsPresenter; import fr.free.nrw.commons.campaigns.CampaignsPresenter;
import fr.free.nrw.commons.campaigns.ICampaignsView; import fr.free.nrw.commons.campaigns.ICampaignsView;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback; import fr.free.nrw.commons.contributions.ContributionsListFragment.Callback;
import fr.free.nrw.commons.contributions.ContributionsListFragment.SourceRefresher;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LatLng;
@ -54,7 +52,6 @@ import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
@ -62,11 +59,10 @@ import timber.log.Timber;
public class ContributionsFragment public class ContributionsFragment
extends CommonsDaggerSupportFragment extends CommonsDaggerSupportFragment
implements implements
MediaDetailProvider,
OnBackStackChangedListener, OnBackStackChangedListener,
SourceRefresher,
LocationUpdateListener, LocationUpdateListener,
ICampaignsView, ContributionsContract.View { MediaDetailProvider,
ICampaignsView, ContributionsContract.View, Callback {
@Inject @Named("default_preferences") JsonKvStore store; @Inject @Named("default_preferences") JsonKvStore store;
@Inject NearbyController nearbyController; @Inject NearbyController nearbyController;
@Inject OkHttpJsonApiClient okHttpJsonApiClient; @Inject OkHttpJsonApiClient okHttpJsonApiClient;
@ -78,8 +74,8 @@ public class ContributionsFragment
private CompositeDisposable compositeDisposable = new CompositeDisposable(); private CompositeDisposable compositeDisposable = new CompositeDisposable();
private ContributionsListFragment contributionsListFragment; private ContributionsListFragment contributionsListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag"; private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
private MediaDetailPagerFragment mediaDetailPagerFragment;
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag"; static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView; @BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
@ -113,7 +109,6 @@ public class ContributionsFragment
} }
}; };
private boolean shouldShowMediaDetailsFragment; private boolean shouldShowMediaDetailsFragment;
private int numberOfContributions;
private boolean isAuthCookieAcquired; private boolean isAuthCookieAcquired;
@Override @Override
@ -128,7 +123,6 @@ public class ContributionsFragment
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
presenter.onAttachView(this); presenter.onAttachView(this);
contributionsPresenter.onAttachView(this); contributionsPresenter.onAttachView(this);
contributionsPresenter.setLifeCycleOwner(this.getViewLifecycleOwner());
campaignView.setVisibility(View.GONE); campaignView.setVisibility(View.GONE);
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null); checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again); checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
@ -141,103 +135,21 @@ public class ContributionsFragment
if (savedInstanceState != null) { if (savedInstanceState != null) {
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager() mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG); .findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager() contributionsListFragment = (ContributionsListFragment) getChildFragmentManager()
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG); .findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible"); shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible");
} }
initFragments(); initFragments();
if(shouldShowMediaDetailsFragment){
showMediaDetailPagerFragment();
}else{
showContributionsListFragment();
}
if (!ConfigUtils.isBetaFlavour()) { if (!ConfigUtils.isBetaFlavour()) {
setUploadCount(); setUploadCount();
} }
getChildFragmentManager().registerFragmentLifecycleCallbacks(
new FragmentManager.FragmentLifecycleCallbacks() {
@Override public void onFragmentResumed(FragmentManager fm, Fragment f) {
super.onFragmentResumed(fm, f);
//If media detail pager fragment is visible, hide the campaigns view [might not be the best way to do, this but yeah, this proves to work for now]
Timber.e("onFragmentResumed %s", f.getClass().getName());
if (f instanceof MediaDetailPagerFragment) {
campaignView.setVisibility(View.GONE);
}
}
@Override public void onFragmentDetached(FragmentManager fm, Fragment f) {
super.onFragmentDetached(fm, f);
Timber.e("onFragmentDetached %s", f.getClass().getName());
//If media detail pager fragment is detached, ContributionsList fragment is gonna be visible, [becomes tightly coupled though]
if (f instanceof MediaDetailPagerFragment) {
fetchCampaigns();
}
}
}, true);
return view; return view;
} }
/**
* Initialose the ContributionsListFragment and MediaDetailPagerFragment fragment
*/
private void initFragments() {
if (null == contributionsListFragment) {
contributionsListFragment = new ContributionsListFragment();
}
contributionsListFragment.setCallback(new Callback() {
@Override
public void retryUpload(Contribution contribution) {
ContributionsFragment.this.retryUpload(contribution);
}
@Override
public void deleteUpload(Contribution contribution) {
contributionsPresenter.deleteUpload(contribution);
}
@Override
public void openMediaDetail(int position) {
showDetail(position);
}
@Override
public Contribution getContributionForPosition(int position) {
return (Contribution) contributionsPresenter.getItemAtPosition(position);
}
@Override
public void fetchMediaUriFor(Contribution contribution) {
Timber.d("Fetching thumbnail for %s", contribution.getFilename());
contributionsPresenter.fetchMediaDetails(contribution);
}
});
if(null==mediaDetailPagerFragment){
mediaDetailPagerFragment=new MediaDetailPagerFragment();
}
}
/**
* Replaces the root frame layout with the given fragment
* @param fragment
* @param tag
*/
private void showFragment(Fragment fragment, String tag) {
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.root_frame, fragment, tag);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
@ -265,7 +177,7 @@ public class ContributionsFragment
if (nearbyNotificationCardView != null) { if (nearbyNotificationCardView != null) {
if (store.getBoolean("displayNearbyCardView", true)) { if (store.getBoolean("displayNearbyCardView", true)) {
if (nearbyNotificationCardView.cardViewVisibilityState if (nearbyNotificationCardView.cardViewVisibilityState
== NearbyNotificationCardView.CardViewVisibilityState.READY) { == NearbyNotificationCardView.CardViewVisibilityState.READY) {
nearbyNotificationCardView.setVisibility(View.VISIBLE); nearbyNotificationCardView.setVisibility(View.VISIBLE);
} }
} else { } else {
@ -275,20 +187,22 @@ public class ContributionsFragment
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG); showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
} }
/**
* Replace FrameLayout with MediaDetailPagerFragment, user will see details of selected media.
* Creates new one if null.
*/
private void showMediaDetailPagerFragment() { private void showMediaDetailPagerFragment() {
// hide tabs on media detail view is visible // hide tabs on media detail view is visible
((MainActivity)getActivity()).hideTabs(); ((MainActivity) getActivity()).hideTabs();
// hide nearby card view on media detail is visible // hide nearby card view on media detail is visible
nearbyNotificationCardView.setVisibility(View.GONE); nearbyNotificationCardView.setVisibility(View.GONE);
showFragment(mediaDetailPagerFragment,MEDIA_DETAIL_PAGER_FRAGMENT_TAG); showFragment(mediaDetailPagerFragment, MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
} }
private void setupViewForMediaDetails() {
campaignView.setVisibility(View.GONE);
nearbyNotificationCardView.setVisibility(View.GONE);
((MainActivity)getActivity()).hideTabs();
}
@Override @Override
public void onBackStackChanged() { public void onBackStackChanged() {
((MainActivity)getActivity()).initBackButton(); ((MainActivity)getActivity()).initBackButton();
@ -307,43 +221,42 @@ public class ContributionsFragment
} }
private void initFragments() {
if (null == contributionsListFragment) {
contributionsListFragment = new ContributionsListFragment(this);
}
if (shouldShowMediaDetailsFragment) {
showMediaDetailPagerFragment();
} else {
showContributionsListFragment();
}
showFragment(contributionsListFragment, CONTRIBUTION_LIST_FRAGMENT_TAG);
}
/**
* Replaces the root frame layout with the given fragment
*
* @param fragment
* @param tag
*/
private void showFragment(Fragment fragment, String tag) {
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.replace(R.id.root_frame, fragment, tag);
transaction.addToBackStack(CONTRIBUTION_LIST_FRAGMENT_TAG);
transaction.commit();
getChildFragmentManager().executePendingTransactions();
}
public Intent getUploadServiceIntent(){ public Intent getUploadServiceIntent(){
Intent intent = new Intent(getActivity(), UploadService.class); Intent intent = new Intent(getActivity(), UploadService.class);
intent.setAction(UploadService.ACTION_START_SERVICE); intent.setAction(UploadService.ACTION_START_SERVICE);
return intent; return intent;
} }
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack.
* Called when user selects a contribution.
*/
private void showDetail(int i) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(i);
}
@Override
public void refreshSource() {
contributionsPresenter.fetchContributions();
}
@Override
public Media getMediaAtPosition(int i) {
return contributionsPresenter.getItemAtPosition(i);
}
@Override
public int getTotalMediaCount() {
return numberOfContributions;
}
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
private void setUploadCount() { private void setUploadCount() {
compositeDisposable.add(okHttpJsonApiClient compositeDisposable.add(okHttpJsonApiClient
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name) .getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
@ -373,8 +286,6 @@ public class ContributionsFragment
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible();
outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible);
} }
@Override @Override
@ -384,13 +295,6 @@ public class ContributionsFragment
firstLocationUpdate = true; firstLocationUpdate = true;
locationManager.addLocationListener(this); locationManager.addLocationListener(this);
boolean isSettingsChanged = store.getBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
store.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, false);
if (isSettingsChanged) {
refreshSource();
}
if (store.getBoolean("displayNearbyCardView", true)) { if (store.getBoolean("displayNearbyCardView", true)) {
checkPermissionsAndShowNearbyCardView(); checkPermissionsAndShowNearbyCardView();
if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) { if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
@ -403,10 +307,6 @@ public class ContributionsFragment
} }
fetchCampaigns(); fetchCampaigns();
if(isAuthCookieAcquired){
contributionsPresenter.fetchContributions();
}
} }
private void checkPermissionsAndShowNearbyCardView() { private void checkPermissionsAndShowNearbyCardView() {
@ -463,17 +363,11 @@ public class ContributionsFragment
} }
private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) { if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) {
Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0); Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0);
String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location); String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location);
closestNearbyPlace.setDistance(distance); closestNearbyPlace.setDistance(distance);
nearbyNotificationCardView.updateContent(closestNearbyPlace); nearbyNotificationCardView.updateContent(closestNearbyPlace);
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
nearbyNotificationCardView.setVisibility(View.GONE);
}else {
nearbyNotificationCardView.setVisibility(View.VISIBLE);
}
} else { } else {
// Means that no close nearby place is found // Means that no close nearby place is found
nearbyNotificationCardView.setVisibility(View.GONE); nearbyNotificationCardView.setVisibility(View.GONE);
@ -553,37 +447,13 @@ public class ContributionsFragment
presenter.onDetachView(); presenter.onDetachView();
} }
@Override
public void showWelcomeTip(boolean shouldShow) {
contributionsListFragment.showWelcomeTip(shouldShow);
}
@Override
public void showProgress(boolean shouldShow) {
contributionsListFragment.showProgress(shouldShow);
}
@Override
public void showNoContributionsUI(boolean shouldShow) {
contributionsListFragment.showNoContributionsUI(shouldShow);
}
@Override
public void setUploadCount(int count) {
this.numberOfContributions=count;
}
@Override
public void showContributions(List<Contribution> contributionList) {
contributionsListFragment.setContributions(contributionList);
}
/** /**
* Retry upload when it is failed * Retry upload when it is failed
* *
* @param contribution contribution to be retried * @param contribution contribution to be retried
*/ */
private void retryUpload(Contribution contribution) { @Override
public void retryUpload(Contribution contribution) {
if (NetworkUtils.isInternetConnectionEstablished(getContext())) { if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
if (contribution.getState() == STATE_FAILED && null != uploadService) { if (contribution.getState() == STATE_FAILED && null != uploadService) {
uploadService.queue(contribution); uploadService.queue(contribution);
@ -596,5 +466,29 @@ public class ContributionsFragment
} }
} }
/**
* Replace whatever is in the current contributionsFragmentContainer view with
* mediaDetailPagerFragment, and preserve previous state in back stack. Called when user selects a
* contribution.
*/
@Override
public void showDetail(int position) {
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
mediaDetailPagerFragment = new MediaDetailPagerFragment();
showMediaDetailPagerFragment();
}
mediaDetailPagerFragment.showImage(position);
}
@Override
public Media getMediaAtPosition(int i) {
return contributionsListFragment.getMediaAtPosition(i);
}
@Override
public int getTotalMediaCount() {
return contributionsListFragment.getTotalMediaCount();
}
} }

View file

@ -1,68 +1,71 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.ViewGroup; import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.DiffUtil;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import java.util.ArrayList;
import java.util.List;
/** /**
* Represents The View Adapter for the List of Contributions * Represents The View Adapter for the List of Contributions
*/ */
public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionViewHolder> { public class ContributionsListAdapter extends
PagedListAdapter<Contribution, ContributionViewHolder> {
private Callback callback; private final Callback callback;
private final MediaClient mediaClient; private final MediaClient mediaClient;
private List<Contribution> contributions;
public ContributionsListAdapter(Callback callback, ContributionsListAdapter(final Callback callback,
MediaClient mediaClient) { final MediaClient mediaClient) {
super(DIFF_CALLBACK);
this.callback = callback; this.callback = callback;
this.mediaClient = mediaClient; this.mediaClient = mediaClient;
contributions = new ArrayList<>();
} }
/** /**
* Creates the new View Holder which will be used to display items(contributions) * Uses DiffUtil to calculate the changes in the list
* using the onBindViewHolder(viewHolder,position) * It has methods that check ID and the content of the items to determine if its a new item
*/ */
@NonNull private static final DiffUtil.ItemCallback<Contribution> DIFF_CALLBACK =
@Override new DiffUtil.ItemCallback<Contribution>() {
public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @Override
ContributionViewHolder viewHolder = new ContributionViewHolder( public boolean areItemsTheSame(final Contribution oldContribution, final Contribution newContribution) {
LayoutInflater.from(parent.getContext()) return oldContribution.getPageId().equals(newContribution.getPageId());
.inflate(R.layout.layout_contribution, parent, false), callback, mediaClient); }
return viewHolder;
}
@Override @Override
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) { public boolean areContentsTheSame(final Contribution oldContribution, final Contribution newContribution) {
final Contribution contribution = contributions.get(position); return oldContribution.equals(newContribution);
if (TextUtils.isEmpty(contribution.getThumbUrl()) }
&& contribution.getState() == Contribution.STATE_COMPLETED) { };
callback.fetchMediaUriFor(contribution);
}
/**
* Initializes the view holder with contribution data
*/
@Override
public void onBindViewHolder(@NonNull final ContributionViewHolder holder, final int position) {
final Contribution contribution = getItem(position);
holder.init(position, contribution); holder.init(position, contribution);
} }
@Override Contribution getContributionForPosition(final int position) {
public int getItemCount() { return getItem(position);
return contributions.size();
}
public void setContributions(@NonNull List<Contribution> contributionList) {
contributions = contributionList;
notifyDataSetChanged();
} }
/**
* Creates the new View Holder which will be used to display items(contributions) using the
* onBindViewHolder(viewHolder,position)
*/
@NonNull
@Override @Override
public long getItemId(int position) { public ContributionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
return contributions.get(position).get_id(); final int viewType) {
final ContributionViewHolder viewHolder = new ContributionViewHolder(
LayoutInflater.from(parent.getContext())
.inflate(R.layout.layout_contribution, parent, false), callback, mediaClient);
return viewHolder;
} }
public interface Callback { public interface Callback {
@ -72,9 +75,5 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
void deleteUpload(Contribution contribution); void deleteUpload(Contribution contribution);
void openMediaDetail(int contribution); void openMediaDetail(int contribution);
Contribution getContributionForPosition(int position);
void fetchMediaUriFor(Contribution contribution);
} }
} }

View file

@ -0,0 +1,24 @@
package fr.free.nrw.commons.contributions;
import fr.free.nrw.commons.BasePresenter;
import java.util.List;
/**
* The contract for Contributions list View & Presenter
*/
public class ContributionsListContract {
public interface View {
void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow);
}
public interface UserActionListener extends BasePresenter<View> {
void deleteUpload(Contribution contribution);
}
}

View file

@ -5,6 +5,7 @@ import static android.view.View.VISIBLE;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -16,218 +17,217 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.LayoutManager; import androidx.recyclerview.widget.RecyclerView.LayoutManager;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.floatingactionbutton.FloatingActionButton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import java.util.ArrayList; import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
/** /**
* Created by root on 01.06.2018. * Created by root on 01.06.2018.
*/ */
public class ContributionsListFragment extends CommonsDaggerSupportFragment { public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
ContributionsListContract.View, ContributionsListAdapter.Callback {
private static final String VISIBLE_ITEM_ID = "visible_item_id"; private static final String RV_STATE = "rv_scroll_state";
@BindView(R.id.contributionsList)
RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery)
FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet)
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
@Inject @Named("default_preferences") JsonKvStore kvStore; @BindView(R.id.contributionsList)
@Inject ContributionController controller; RecyclerView rvContributionsList;
@Inject MediaClient mediaClient; @BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery)
FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet)
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
private Animation fab_close; @Inject
private Animation fab_open; ContributionController controller;
private Animation rotate_forward; @Inject
private Animation rotate_backward; MediaClient mediaClient;
@Inject
ContributionsListPresenter contributionsListPresenter;
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
private boolean isFabOpen = false; private boolean isFabOpen;
private ContributionsListAdapter adapter; private ContributionsListAdapter adapter;
private Callback callback; private final Callback callback;
private String lastVisibleItemID;
private int SPAN_COUNT=3; private final int SPAN_COUNT_LANDSCAPE = 3;
private List<Contribution> contributions=new ArrayList<>(); private final int SPAN_COUNT_PORTRAIT = 1;
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { ContributionsListFragment(final Callback callback) {
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false); this.callback = callback;
ButterKnife.bind(this, view); }
initAdapter();
return view; public View onCreateView(
final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
contributionsListPresenter.onAttachView(this);
initAdapter();
return view;
}
private void initAdapter() {
adapter = new ContributionsListAdapter(this, mediaClient);
}
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
private void initRecyclerView() {
final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
getSpanCount(getResources().getConfiguration().orientation));
rvContributionsList.setLayoutManager(layoutManager);
contributionsListPresenter.setup();
contributionsListPresenter.contributionList.observe(this, adapter::submitList);
rvContributionsList.setAdapter(adapter);
}
private int getSpanCount(final int orientation) {
return orientation == Configuration.ORIENTATION_LANDSCAPE ?
SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
}
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
private void animateFAB(final boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()) {
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
}
this.isFabOpen = !isFabOpen;
} }
}
public void setCallback(Callback callback) { /**
this.callback = callback; * Shows welcome message if user has no contributions yet i.e. new user.
*/
public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void showNoContributionsUI(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList.getLayoutManager();
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (null != savedInstanceState) {
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
} }
}
private void initAdapter() { @Override
adapter = new ContributionsListAdapter(callback, mediaClient); public void retryUpload(final Contribution contribution) {
adapter.setHasStableIds(true); callback.retryUpload(contribution);
} }
@Override @Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { public void deleteUpload(final Contribution contribution) {
super.onViewCreated(view, savedInstanceState); contributionsListPresenter.deleteUpload(contribution);
initRecyclerView(); }
initializeAnimations();
setListeners();
}
private void initRecyclerView() { @Override
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) { public void openMediaDetail(final int position) {
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT)); callback.showDetail(position);
} else { }
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
}
rvContributionsList.setAdapter(adapter); public Media getMediaAtPosition(final int i) {
adapter.setContributions(contributions); return adapter.getContributionForPosition(i);
} }
@Override public int getTotalMediaCount() {
public void onConfigurationChanged(Configuration newConfig) { return adapter.getItemCount();
super.onConfigurationChanged(newConfig); }
// check orientation
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
fab_layout.setOrientation(LinearLayout.HORIZONTAL);
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
fab_layout.setOrientation(LinearLayout.VERTICAL);
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
}
}
private void initializeAnimations() { public interface Callback {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
private void setListeners() { void retryUpload(Contribution contribution);
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
private void animateFAB(boolean isFabOpen) {
this.isFabOpen = !isFabOpen;
if (fabPlus.isShown()){
if (isFabOpen) {
fabPlus.startAnimation(rotate_backward);
fabCamera.startAnimation(fab_close);
fabGallery.startAnimation(fab_close);
fabCamera.hide();
fabGallery.hide();
} else {
fabPlus.startAnimation(rotate_forward);
fabCamera.startAnimation(fab_open);
fabGallery.startAnimation(fab_open);
fabCamera.show();
fabGallery.show();
}
this.isFabOpen=!isFabOpen;
}
}
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
public void showWelcomeTip(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
public void showProgress(boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void showNoContributionsUI(boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
public void setContributions(List<Contribution> contributionList) {
this.contributions.clear();
this.contributions.addAll(contributionList);
adapter.setContributions(contributions);
}
public interface SourceRefresher {
void refreshSource();
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
LayoutManager layoutManager = rvContributionsList.getLayoutManager();
int lastVisibleItemPosition=0;
if(layoutManager instanceof LinearLayoutManager){
lastVisibleItemPosition= ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition();
}else if(layoutManager instanceof GridLayoutManager){
lastVisibleItemPosition=((GridLayoutManager)layoutManager).findLastCompletelyVisibleItemPosition();
}
String idOfItemWithPosition = findIdOfItemWithPosition(lastVisibleItemPosition);
if (null != idOfItemWithPosition) {
outState.putString(VISIBLE_ITEM_ID, idOfItemWithPosition);
}
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if(null!=savedInstanceState){
lastVisibleItemID =savedInstanceState.getString(VISIBLE_ITEM_ID, null);
}
}
/**
* Gets the id of the contribution from the db
* @param position
* @return
*/
@Nullable
private String findIdOfItemWithPosition(int position) {
Contribution contributionForPosition = callback.getContributionForPosition(position);
if (null != contributionForPosition) {
return contributionForPosition.getFilename();
}
return null;
}
void showDetail(int position);
}
} }

View file

@ -0,0 +1,71 @@
package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.contributions.ContributionsListContract.UserActionListener;
import fr.free.nrw.commons.di.CommonsApplicationModule;
import io.reactivex.Scheduler;
import io.reactivex.disposables.CompositeDisposable;
import javax.inject.Inject;
import javax.inject.Named;
/**
* The presenter class for Contributions
*/
public class ContributionsListPresenter implements UserActionListener {
private final ContributionBoundaryCallback contributionBoundaryCallback;
private final ContributionsRepository repository;
private final Scheduler ioThreadScheduler;
private final CompositeDisposable compositeDisposable;
LiveData<PagedList<Contribution>> contributionList;
@Inject
ContributionsListPresenter(
final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(final ContributionsListContract.View view) {
}
/**
* Setup the paged list. This method sets the configuration for paged list and ties it up with the
* live data object. This method can be tweaked to update the lazy loading behavior of the
* contributions list
*/
void setup() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
contributionList = (new LivePagedListBuilder(repository.fetchContributions(), pagedListConfig)
.setBoundaryCallback(contributionBoundaryCallback)).build();
}
@Override
public void onDetachView() {
compositeDisposable.clear();
}
/**
* Delete a failed contribution from the local db
*/
@Override
public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}

View file

@ -1,14 +1,13 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData; import androidx.paging.DataSource.Factory;
import io.reactivex.Completable;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.kvstore.JsonKvStore;
import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
/** /**
@ -59,23 +58,23 @@ class ContributionsLocalDataSource {
* @param contribution * @param contribution
* @return * @return
*/ */
public Single<Integer> deleteContribution(Contribution contribution) { public Completable deleteContribution(Contribution contribution) {
return contributionDao.delete(contribution); return contributionDao.delete(contribution);
} }
public LiveData<List<Contribution>> getContributions() { public Factory<Integer, Contribution> getContributions() {
return contributionDao.fetchContributions(); return contributionDao.fetchContributions();
} }
public Completable saveContributions(List<Contribution> contributions) { public Single<List<Long>> saveContributions(List<Contribution> contributions) {
return contributionDao.deleteAllAndSave(contributions); return contributionDao.save(contributions);
} }
public void set(String key, long value) { public void set(String key, long value) {
defaultKVStore.putLong(key,value); defaultKVStore.putLong(key,value);
} }
public Single<Integer> updateContribution(Contribution contribution) { public Completable updateContribution(Contribution contribution) {
return contributionDao.update(contribution); return contributionDao.update(contribution);
} }
} }

View file

@ -9,8 +9,8 @@ import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.MediaDataExtractor;
import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener; import fr.free.nrw.commons.contributions.ContributionsContract.UserActionListener;
import fr.free.nrw.commons.db.AppDatabase;
import fr.free.nrw.commons.di.CommonsApplicationModule; import fr.free.nrw.commons.di.CommonsApplicationModule;
import fr.free.nrw.commons.mwapi.UserClient; import fr.free.nrw.commons.mwapi.UserClient;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
@ -25,6 +25,9 @@ import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
import javax.inject.Inject;
import javax.inject.Named;
/** /**
* The presenter class for Contributions * The presenter class for Contributions
*/ */
@ -35,25 +38,10 @@ public class ContributionsPresenter implements UserActionListener {
private final Scheduler ioThreadScheduler; private final Scheduler ioThreadScheduler;
private CompositeDisposable compositeDisposable; private CompositeDisposable compositeDisposable;
private ContributionsContract.View view; private ContributionsContract.View view;
private List<Contribution> contributionList=new ArrayList<>();
@Inject
Context context;
@Inject
UserClient userClient;
@Inject
AppDatabase appDatabase;
@Inject
SessionManager sessionManager;
@Inject @Inject
MediaDataExtractor mediaDataExtractor; MediaDataExtractor mediaDataExtractor;
private LifecycleOwner lifeCycleOwner;
@Inject @Inject
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
this.repository = repository; this.repository = repository;
@ -61,74 +49,12 @@ public class ContributionsPresenter implements UserActionListener {
this.ioThreadScheduler=ioThreadScheduler; this.ioThreadScheduler=ioThreadScheduler;
} }
private String user;
@Override @Override
public void onAttachView(ContributionsContract.View view) { public void onAttachView(ContributionsContract.View view) {
this.view = view; this.view = view;
compositeDisposable=new CompositeDisposable(); compositeDisposable=new CompositeDisposable();
} }
public void setLifeCycleOwner(LifecycleOwner lifeCycleOwner){
this.lifeCycleOwner=lifeCycleOwner;
}
public void fetchContributions() {
Timber.d("fetch Contributions");
LiveData<List<Contribution>> liveDataContributions = repository.fetchContributions();
if(null!=lifeCycleOwner) {
liveDataContributions.observe(lifeCycleOwner, this::showContributions);
}
if (NetworkUtils.isInternetConnectionEstablished(CommonsApplication.getInstance()) && shouldFetchContributions()) {
Timber.d("fetching contributions: ");
view.showProgress(true);
this.user = sessionManager.getUserName();
view.showContributions(Collections.emptyList());
compositeDisposable.add(userClient.logEvents(user)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.doOnNext(mwQueryLogEvent -> Timber.d("Received image %s", mwQueryLogEvent.title()))
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
.map(image -> new Contribution(image, user))
.toList()
.subscribe(this::saveContributionsToDB, error -> {
Timber.e("Failed to fetch contributions: %s", error.getMessage());
}));
}
}
private void showContributions(@NonNull List<Contribution> contributions) {
view.showProgress(false);
if (contributions.isEmpty()) {
view.showWelcomeTip(true);
view.showNoContributionsUI(true);
} else {
view.showWelcomeTip(false);
view.showNoContributionsUI(false);
view.setUploadCount(contributions.size());
view.showContributions(contributions);
this.contributionList.clear();
this.contributionList.addAll(contributions);
}
}
private void saveContributionsToDB(List<Contribution> contributions) {
Timber.e("Fetched: "+contributions.size()+" contributions "+" saving to db");
repository.save(contributions).subscribeOn(ioThreadScheduler).subscribe();
repository.set("last_fetch_timestamp",System.currentTimeMillis());
}
private boolean shouldFetchContributions() {
long lastFetchTimestamp = repository.getLong("last_fetch_timestamp");
Timber.d("last fetch timestamp: %s", lastFetchTimestamp);
if(lastFetchTimestamp!=0){
return System.currentTimeMillis()-lastFetchTimestamp>15*60*100;
}
Timber.d("should fetch contributions: %s", true);
return true;
}
@Override @Override
public void onDetachView() { public void onDetachView() {
this.view = null; this.view = null;
@ -146,24 +72,10 @@ public class ContributionsPresenter implements UserActionListener {
*/ */
@Override @Override
public void deleteUpload(Contribution contribution) { public void deleteUpload(Contribution contribution) {
compositeDisposable.add(repository.deleteContributionFromDB(contribution) compositeDisposable.add(repository
.subscribeOn(ioThreadScheduler) .deleteContributionFromDB(contribution)
.subscribe()); .subscribeOn(ioThreadScheduler)
} .subscribe());
/**
* Returns a contribution at the specified cursor position
*
* @param i
* @return
*/
@Nullable
@Override
public Media getItemAtPosition(int i) {
if (i == -1 || contributionList.size() < i+1) {
return null;
}
return contributionList.get(i);
} }
@Override @Override

View file

@ -1,12 +1,11 @@
package fr.free.nrw.commons.contributions; package fr.free.nrw.commons.contributions;
import androidx.lifecycle.LiveData; import androidx.paging.DataSource.Factory;
import io.reactivex.Completable;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import io.reactivex.Completable;
import io.reactivex.Single; import io.reactivex.Single;
/** /**
@ -33,7 +32,7 @@ public class ContributionsRepository {
* @param contribution * @param contribution
* @return * @return
*/ */
public Single<Integer> deleteContributionFromDB(Contribution contribution) { public Completable deleteContributionFromDB(Contribution contribution) {
return localDataSource.deleteContribution(contribution); return localDataSource.deleteContribution(contribution);
} }
@ -46,11 +45,11 @@ public class ContributionsRepository {
return localDataSource.getContributionWithFileName(fileName); return localDataSource.getContributionWithFileName(fileName);
} }
public LiveData<List<Contribution>> fetchContributions() { public Factory<Integer, Contribution> fetchContributions() {
return localDataSource.getContributions(); return localDataSource.getContributions();
} }
public Completable save(List<Contribution> contributions) { public Single<List<Long>> save(List<Contribution> contributions) {
return localDataSource.saveContributions(contributions); return localDataSource.saveContributions(contributions);
} }
@ -58,11 +57,7 @@ public class ContributionsRepository {
localDataSource.set(key,value); localDataSource.set(key,value);
} }
public long getLong(String key) { public Completable updateContribution(Contribution contribution) {
return localDataSource.getLong(key);
}
public Single<Integer> updateContribution(Contribution contribution) {
return localDataSource.updateContribution(contribution); return localDataSource.updateContribution(contribution);
} }
} }

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -12,7 +11,6 @@ import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.view.GravityCompat; import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout; import androidx.drawerlayout.widget.DrawerLayout;
@ -20,16 +18,9 @@ import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter; import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager; import androidx.viewpager.widget.ViewPager;
import com.google.android.material.tabs.TabLayout;
import java.util.List;
import javax.inject.Inject;
import butterknife.BindView; import butterknife.BindView;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import fr.free.nrw.commons.BuildConfig; import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.location.LocationServiceManager; import fr.free.nrw.commons.location.LocationServiceManager;
@ -44,10 +35,10 @@ import fr.free.nrw.commons.upload.UploadService;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber; import timber.log.Timber;
import static android.content.ContentResolver.requestSync;
public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener { public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
@BindView(R.id.tab_layout) @BindView(R.id.tab_layout)

View file

@ -13,7 +13,7 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
public class DBOpenHelper extends SQLiteOpenHelper { public class DBOpenHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "commons.db"; private static final String DATABASE_NAME = "commons.db";
private static final int DATABASE_VERSION = 12; private static final int DATABASE_VERSION = 13;
public static final String CONTRIBUTIONS_TABLE = "contributions"; public static final String CONTRIBUTIONS_TABLE = "contributions";
private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s"; private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s";

View file

@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
* The database for accessing the respective DAOs * The database for accessing the respective DAOs
* *
*/ */
@Database(entities = [Contribution::class], version = 1, exportSchema = false) @Database(entities = [Contribution::class], version = 2, exportSchema = false)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao abstract fun contributionDao(): ContributionDao

View file

@ -90,8 +90,9 @@ public interface DepictedImagesContract {
/** /**
* Fetches more images for the item and adds it to the grid view adapter * Fetches more images for the item and adds it to the grid view adapter
* @param entityId
*/ */
void fetchMoreImages(); void fetchMoreImages(String entityId);
/** /**
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available) * fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)

View file

@ -136,7 +136,7 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) { if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet(); handleNoInternet();
} else { } else {
presenter.fetchMoreImages(); presenter.fetchMoreImages(entityId);
} }
} }
if (isLastPage) { if (isLastPage) {

View file

@ -80,10 +80,11 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
/** /**
* Fetches more images for the item and adds it to the grid view adapter * Fetches more images for the item and adds it to the grid view adapter
* @param entityId
*/ */
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
@Override @Override
public void fetchMoreImages() { public void fetchMoreImages(String entityId) {
view.progressBarVisible(true); view.progressBarVisible(true);
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, queryList.size()) compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, queryList.size())
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)

View file

@ -224,7 +224,9 @@ public class CommonsApplicationModule {
@Provides @Provides
@Singleton @Singleton
public AppDatabase provideAppDataBase() { public AppDatabase provideAppDataBase() {
return Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db").build(); return Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db")
.fallbackToDestructiveMigration()
.build();
} }
@Provides @Provides

View file

@ -4,10 +4,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.paging.PagedListAdapter import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.BaseViewHolder
import fr.free.nrw.commons.explore.inflate import fr.free.nrw.commons.explore.inflate
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_depictions.* import kotlinx.android.synthetic.main.item_depictions.*
@ -22,18 +23,17 @@ class DepictionAdapter(val onDepictionClicked: (DepictedItem) -> Unit) :
} }
) { ) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DepictedItemViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DepictedItemViewHolder {
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions), onDepictionClicked) return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions))
} }
override fun onBindViewHolder(holder: DepictedItemViewHolder, position: Int) { override fun onBindViewHolder(holder: DepictedItemViewHolder, position: Int) {
holder.bind(getItem(position)!!) holder.bind(getItem(position)!!, onDepictionClicked)
} }
} }
class DepictedItemViewHolder(containerView: View, val onDepictionClicked: (DepictedItem) -> Unit) : class DepictedItemViewHolder(override val containerView: View) :
BaseViewHolder<DepictedItem>(containerView) { RecyclerView.ViewHolder(containerView), LayoutContainer {
fun bind(item: DepictedItem, onDepictionClicked: (DepictedItem) -> Unit) {
override fun bind(item: DepictedItem) {
containerView.setOnClickListener { onDepictionClicked(item) } containerView.setOnClickListener { onDepictionClicked(item) }
depicts_label.text = item.name depicts_label.text = item.name
description.text = item.description description.text = item.description
@ -44,7 +44,3 @@ class DepictedItemViewHolder(containerView: View, val onDepictionClicked: (Depic
} }
} }
} }

View file

@ -75,7 +75,7 @@ class DepictsClient @Inject constructor(
} }
fun getEntities(ids: String): Single<Entities> { fun getEntities(ids: String): Single<Entities> {
return depictsInterface.getEntities(ids, Locale.getDefault().language) return depictsInterface.getEntities(ids)
} }
fun toDepictions(sparqlResponse: Observable<SparqlResponse>): Observable<List<DepictedItem>> { fun toDepictions(sparqlResponse: Observable<SparqlResponse>): Observable<List<DepictedItem>> {

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.explore.depictions
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.R
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.list_item_load_more.*
class FooterAdapter(private val onRefreshClicked: () -> Unit) :
ListAdapter<FooterItem, FooterViewHolder>(object :
DiffUtil.ItemCallback<FooterItem>() {
override fun areItemsTheSame(oldItem: FooterItem, newItem: FooterItem) = oldItem == newItem
override fun areContentsTheSame(oldItem: FooterItem, newItem: FooterItem) =
oldItem == newItem
}) {
override fun getItemViewType(position: Int): Int {
return getItem(position).ordinal
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
when (FooterItem.values()[viewType]) {
FooterItem.LoadingItem -> LoadingViewHolder(parent.inflate(R.layout.list_item_progress))
FooterItem.RefreshItem -> RefreshViewHolder(
parent.inflate(R.layout.list_item_load_more),
onRefreshClicked
)
}
override fun onBindViewHolder(holder: FooterViewHolder, position: Int) {}
}
open class FooterViewHolder(override val containerView: View) :
RecyclerView.ViewHolder(containerView),
LayoutContainer
class LoadingViewHolder(containerView: View) : FooterViewHolder(containerView)
class RefreshViewHolder(containerView: View, onRefreshClicked: () -> Unit) :
FooterViewHolder(containerView) {
init {
listItemLoadMoreButton.setOnClickListener { onRefreshClicked() }
}
}
enum class FooterItem { LoadingItem, RefreshItem }
fun ViewGroup.inflate(@LayoutRes layoutId: Int, attachToRoot: Boolean = false): View =
LayoutInflater.from(context).inflate(layoutId, this, attachToRoot)

View file

@ -0,0 +1,63 @@
package fr.free.nrw.commons.explore.depictions
import androidx.paging.PositionalDataSource
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Completable
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
data class SearchDepictionsDataSource constructor(
private val depictsClient: DepictsClient,
private val loadingStates: PublishProcessor<LoadingState>,
private val query: String
) : PositionalDataSource<DepictedItem>() {
private var lastExecutedRequest: (() -> Boolean)? = null
override fun loadInitial(
params: LoadInitialParams,
callback: LoadInitialCallback<DepictedItem>
) {
storeAndExecute {
loadingStates.offer(LoadingState.InitialLoad)
performWithTryCatch {
callback.onResult(
getItems(query, params.requestedLoadSize, params.requestedStartPosition),
params.requestedStartPosition
)
}
}
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<DepictedItem>) {
storeAndExecute {
loadingStates.offer(LoadingState.Loading)
performWithTryCatch {
callback.onResult(getItems(query, params.loadSize, params.startPosition))
}
}
}
fun retryFailedRequest() {
Completable.fromAction { lastExecutedRequest?.invoke() }
.subscribeOn(Schedulers.io())
.subscribe()
}
private fun getItems(query: String, limit: Int, offset: Int) =
depictsClient.searchForDepictions(query, limit, offset).blockingGet()
private fun storeAndExecute(function: () -> Boolean) {
function.also { lastExecutedRequest = it }.invoke()
}
private fun performWithTryCatch(function: () -> Unit) = try {
function.invoke()
loadingStates.offer(LoadingState.Complete)
} catch (e: Exception) {
Timber.e(e)
loadingStates.offer(LoadingState.Error)
}
}

View file

@ -0,0 +1,88 @@
package fr.free.nrw.commons.explore.depictions
import androidx.lifecycle.LiveData
import androidx.paging.Config
import androidx.paging.DataSource
import androidx.paging.PagedList
import androidx.paging.toLiveData
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Flowable
import io.reactivex.processors.PublishProcessor
import javax.inject.Inject
private const val PAGE_SIZE = 50
private const val INITIAL_LOAD_SIZE = 50
class SearchableDepictionsDataSourceFactory @Inject constructor(
val searchDepictionsDataSourceFactoryFactory: SearchDepictionsDataSourceFactoryFactory,
val liveDataConverter: LiveDataConverter
) {
private val _loadingStates = PublishProcessor.create<LoadingState>()
val loadingStates: Flowable<LoadingState> = _loadingStates
private val _searchResults = PublishProcessor.create<LiveData<PagedList<DepictedItem>>>()
val searchResults: Flowable<LiveData<PagedList<DepictedItem>>> = _searchResults
private val _noItemsLoadedEvent = PublishProcessor.create<Unit>()
val noItemsLoadedEvent: Flowable<Unit> = _noItemsLoadedEvent
private var currentFactory: SearchDepictionsDataSourceFactory? = null
fun onQueryUpdated(query: String) {
_searchResults.offer(
liveDataConverter.convert(
searchDepictionsDataSourceFactoryFactory.create(query, _loadingStates)
.also { currentFactory = it }
) { _noItemsLoadedEvent.offer(Unit) }
)
}
fun retryFailedRequest() {
currentFactory?.retryFailedRequest()
}
}
class LiveDataConverter @Inject constructor() {
fun convert(
dataSourceFactory: SearchDepictionsDataSourceFactory,
zeroItemsLoadedFunction: () -> Unit
): LiveData<PagedList<DepictedItem>> {
return dataSourceFactory.toLiveData(
Config(
pageSize = PAGE_SIZE,
initialLoadSizeHint = INITIAL_LOAD_SIZE,
enablePlaceholders = false
),
boundaryCallback = object : PagedList.BoundaryCallback<DepictedItem>() {
override fun onZeroItemsLoaded() {
zeroItemsLoadedFunction()
}
}
)
}
}
interface SearchDepictionsDataSourceFactoryFactory {
fun create(query: String, loadingStates: PublishProcessor<LoadingState>)
: SearchDepictionsDataSourceFactory
}
class SearchDepictionsDataSourceFactory constructor(
private val depictsClient: DepictsClient,
private val query: String,
private val loadingStates: PublishProcessor<LoadingState>
) : DataSource.Factory<Int, DepictedItem>() {
private var currentDataSource: SearchDepictionsDataSource? = null
override fun create() = SearchDepictionsDataSource(depictsClient, loadingStates, query)
.also { currentDataSource = it }
fun retryFailedRequest() {
currentDataSource?.retryFailedRequest()
}
}
sealed class LoadingState {
object InitialLoad : LoadingState()
object Loading : LoadingState()
object Complete : LoadingState()
object Error : LoadingState()
}

View file

@ -32,6 +32,7 @@ public class MediaClient {
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why. //OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
private Map<String, Map<String, String>> continuationStore; private Map<String, Map<String, String>> continuationStore;
private Map<String, Boolean> continuationExists;
public static final String NO_CAPTION = "No caption"; public static final String NO_CAPTION = "No caption";
private static final String NO_DEPICTION = "No depiction"; private static final String NO_DEPICTION = "No depiction";
@ -40,6 +41,7 @@ public class MediaClient {
this.mediaInterface = mediaInterface; this.mediaInterface = mediaInterface;
this.mediaDetailInterface = mediaDetailInterface; this.mediaDetailInterface = mediaDetailInterface;
this.continuationStore = new HashMap<>(); this.continuationStore = new HashMap<>();
this.continuationExists = new HashMap<>();
} }
/** /**
@ -83,6 +85,36 @@ public class MediaClient {
} }
/**
* This method takes the userName as input and returns a list of Media objects filtered using
* allimages query It uses the allimages query API to get the images contributed by the userName,
* 10 at a time.
*
* @param userName the username
* @return
*/
public Single<List<Media>> getMediaListForUser(String userName) {
Map<String, String> continuation =
continuationStore.containsKey("user_" + userName)
? continuationStore.get("user_" + userName)
: Collections.emptyMap();
return responseToMediaList(mediaInterface
.getMediaListForUser(userName, 10, continuation), "user_" + userName);
}
/**
* Check if media for user has reached the end of the list.
* @param userName
* @return
*/
public boolean doesMediaListForUserHaveMorePages(String userName) {
final String key = "user_" + userName;
if(continuationExists.containsKey(key)) {
return continuationExists.get(key);
}
return true;
}
/** /**
* This method takes a keyword as input and returns a list of Media objects filtered using image generator query * This method takes a keyword as input and returns a list of Media objects filtered using image generator query
* It uses the generator query API to get the images searched using a query, 10 at a time. * It uses the generator query API to get the images searched using a query, 10 at a time.
@ -104,7 +136,12 @@ public class MediaClient {
|| null == mwQueryResponse.query().pages()) { || null == mwQueryResponse.query().pages()) {
return Observable.empty(); return Observable.empty();
} }
continuationStore.put(key, mwQueryResponse.continuation()); if(mwQueryResponse.continuation() != null) {
continuationStore.put(key, mwQueryResponse.continuation());
continuationExists.put(key, true);
} else {
continuationExists.put(key, false);
}
return Observable.fromIterable(mwQueryResponse.query().pages()); return Observable.fromIterable(mwQueryResponse.query().pages());
}) })
.map(Media::from) .map(Media::from)
@ -169,7 +206,7 @@ public class MediaClient {
* @return caption for image using wikibaseIdentifier * @return caption for image using wikibaseIdentifier
*/ */
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) { public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier) return mediaDetailInterface.getEntityForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
.map(mediaDetailResponse -> { .map(mediaDetailResponse -> {
if (isSuccess(mediaDetailResponse)) { if (isSuccess(mediaDetailResponse)) {
for (Entity wikibaseItem : mediaDetailResponse.entities().values()) { for (Entity wikibaseItem : mediaDetailResponse.entities().values()) {
@ -205,12 +242,16 @@ public class MediaClient {
* @param entityId EntityId (Ex: Q81566) of the depict entity * @param entityId EntityId (Ex: Q81566) of the depict entity
* @return label * @return label
*/ */
public Single<String> getLabelForDepiction(String entityId) { public Single<String> getLabelForDepiction(String entityId, String language) {
return getEntities(entityId) return mediaDetailInterface.getEntity(entityId)
.map(entities -> { .map(entities -> {
if (isSuccess(entities)) { if (isSuccess(entities)) {
for (Entity entity : entities.entities().values()) { for (Entity entity : entities.entities().values()) {
for (Label label : entity.labels().values()) { final Map<String, Label> languageToLabelMap = entity.labels();
if (languageToLabelMap.containsKey(language)) {
return languageToLabelMap.get(language).value();
}
for (Label label : languageToLabelMap.values()) {
return label.value(); return label.value();
} }
} }
@ -220,6 +261,6 @@ public class MediaClient {
} }
public Single<Entities> getEntities(String entityId) { public Single<Entities> getEntities(String entityId) {
return mediaDetailInterface.getEntity(entityId, Locale.getDefault().getLanguage()); return mediaDetailInterface.getEntity(entityId);
} }
} }

View file

@ -51,7 +51,6 @@ import fr.free.nrw.commons.delete.ReasonBuilder;
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity; import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.ui.widget.HtmlTextView; import fr.free.nrw.commons.ui.widget.HtmlTextView;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ViewUtilWrapper; import fr.free.nrw.commons.utils.ViewUtilWrapper;
import io.reactivex.Single; import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.android.schedulers.AndroidSchedulers;

View file

@ -21,12 +21,11 @@ public interface MediaDetailInterface {
/** /**
* Gets labels for Depictions using Entity Id from MediaWikiAPI * Gets labels for Depictions using Entity Id from MediaWikiAPI
* @param entityId EntityId (Ex: Q81566) of the depict entity
* *
* @param entityId EntityId (Ex: Q81566) of the depict entity
* @param language user's locale
*/ */
@GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1") @GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1")
Single<Entities> getEntity(@Query("ids") String entityId, @Query("languages") String language); Single<Entities> getEntity(@Query("ids") String entityId);
/** /**
* Fetches caption using wikibaseIdentifier * Fetches caption using wikibaseIdentifier
@ -34,5 +33,5 @@ public interface MediaDetailInterface {
* @param wikibaseIdentifier pageId for the media * @param wikibaseIdentifier pageId for the media
*/ */
@GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki") @GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
Observable<Entities> getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier); Observable<Entities> getEntityForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
} }

View file

@ -23,26 +23,21 @@ import butterknife.ButterKnife;
import com.google.android.material.snackbar.Snackbar; import com.google.android.material.snackbar.Snackbar;
import fr.free.nrw.commons.Media; import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.Bookmark; import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.category.CategoryImagesCallback; import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.utils.DownloadUtils; import fr.free.nrw.commons.utils.DownloadUtils;
import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil; import fr.free.nrw.commons.utils.ViewUtil;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import timber.log.Timber; import timber.log.Timber;
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener { public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
@Inject SessionManager sessionManager;
@Inject @Named("default_preferences") JsonKvStore store;
@Inject BookmarkPicturesDao bookmarkDao; @Inject BookmarkPicturesDao bookmarkDao;
@BindView(R.id.mediaDetailsPager) ViewPager pager; @BindView(R.id.mediaDetailsPager) ViewPager pager;

View file

@ -16,6 +16,7 @@ public interface MediaInterface {
String MEDIA_PARAMS="&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" + String MEDIA_PARAMS="&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" +
"&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" + "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
"|Artist|LicenseShortName|LicenseUrl"; "|Artist|LicenseShortName|LicenseUrl";
/** /**
* Checks if a page exists or not. * Checks if a page exists or not.
* *
@ -47,6 +48,19 @@ public interface MediaInterface {
MEDIA_PARAMS) MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation); Observable<MwQueryResponse> getMediaListFromCategory(@Query("gcmtitle") String category, @Query("gcmlimit") int itemLimit, @QueryMap Map<String, String> continuation);
/**
* This method retrieves a list of Media objects for a given user name
*
* @param username user's Wikimedia Commons username.
* @param itemLimit how many images are returned
* @param continuation the continuation string from the previous query or empty map
* @return
*/
@GET("w/api.php?action=query&format=json&formatversion=2" + //Basic parameters
"&generator=allimages&gaisort=timestamp&gaidir=older" + MEDIA_PARAMS)
Observable<MwQueryResponse> getMediaListForUser(@Query("gaiuser") String username,
@Query("gailimit") int itemLimit, @QueryMap(encoded = true) Map<String, String> continuation);
/** /**
* This method retrieves a list of Media objects filtered using image generator query * This method retrieves a list of Media objects filtered using image generator query
* *
@ -85,21 +99,15 @@ public interface MediaInterface {
Observable<MwParseResponse> getPageHtml(@Query("page") String title); Observable<MwParseResponse> getPageHtml(@Query("page") String title);
/** /**
* Fetches caption using file name * Fetches list of images from a depiction entity
* *
* @param filename name of the file to be used for fetching captions * @param query depictionEntityId
* */ * @param sroffset number od depictions already fetched, this is useful in implementing
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1") * pagination
Observable<MwQueryResponse> fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename); */
/** @GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
* Fetches list of images from a depiction entity Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query,
* @Query("sroffset") String sroffset);
* @param query depictionEntityId
* @param sroffset number od depictions already fetched, this is useful in implementing pagination
*/
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset);
} }

View file

@ -4,7 +4,9 @@ import android.os.Parcel;
import androidx.annotation.DrawableRes; import androidx.annotation.DrawableRes;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
@ -17,6 +19,7 @@ import fr.free.nrw.commons.R;
*/ */
public enum Label { public enum Label {
BOOKMARKS("BOOKMARK", R.drawable.ic_filled_star_24dp),
BUILDING("Q41176", R.drawable.round_icon_generic_building), BUILDING("Q41176", R.drawable.round_icon_generic_building),
HOUSE("Q3947", R.drawable.round_icon_house), HOUSE("Q3947", R.drawable.round_icon_house),
COTTAGE("Q5783996", R.drawable.round_icon_house), COTTAGE("Q5783996", R.drawable.round_icon_house),
@ -92,4 +95,8 @@ public enum Label {
Label label = TEXT_TO_DESCRIPTION.get(text); Label label = TEXT_TO_DESCRIPTION.get(text);
return label == null ? UNKNOWN : label; return label == null ? UNKNOWN : label;
} }
public static List<Label> valuesAsList() {
return Arrays.asList(Label.values());
}
} }

View file

@ -4,6 +4,7 @@ import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import androidx.annotation.MainThread;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.mapbox.mapboxsdk.annotations.IconFactory; import com.mapbox.mapboxsdk.annotations.IconFactory;
@ -14,6 +15,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.ListIterator;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -166,36 +168,6 @@ public class NearbyController {
VectorDrawableCompat vectorDrawable = null; VectorDrawableCompat vectorDrawable = null;
VectorDrawableCompat vectorDrawableGreen = null; VectorDrawableCompat vectorDrawableGreen = null;
VectorDrawableCompat vectorDrawableGrey = null; VectorDrawableCompat vectorDrawableGrey = null;
try {
vectorDrawable = VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_custom_bookmark_marker, context.getTheme()
);
} catch (Resources.NotFoundException e) {
// ignore when running tests.
}
if (vectorDrawable != null) {
Bitmap icon = UiUtils.getBitmap(vectorDrawable);
for (Place place : bookmarkplacelist) {
String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(IconFactory.getInstance(context)
.fromBitmap(icon));
placeList.remove(place);
baseMarkerOptions.add(nearbyBaseMarker);
}
}
vectorDrawable = null; vectorDrawable = null;
try { try {
vectorDrawable = VectorDrawableCompat.create( vectorDrawable = VectorDrawableCompat.create(
@ -255,4 +227,19 @@ public class NearbyController {
public LatLng curLatLng; // Current location when this places are populated public LatLng curLatLng; // Current location when this places are populated
public LatLng searchLatLng; // Search location for finding this places public LatLng searchLatLng; // Search location for finding this places
} }
/**
* Updates makerLabelList item isBookmarked value
* @param place place which is bookmarked
* @param isBookmarked true is bookmarked, false if bookmark removed
*/
@MainThread
public static void updateMarkerLabelListBookmark(Place place, boolean isBookmarked) {
for (ListIterator<MarkerPlaceGroup> iter = markerLabelList.listIterator(); iter.hasNext();) {
MarkerPlaceGroup markerPlaceGroup = iter.next();
if (markerPlaceGroup.getPlace().getWikiDataEntityId().equals(place.getWikiDataEntityId())) {
iter.set(new MarkerPlaceGroup(markerPlaceGroup.getMarker(), isBookmarked, place));
}
}
}
} }

View file

@ -20,6 +20,7 @@ import java.util.ArrayList;
import fr.free.nrw.commons.R; import fr.free.nrw.commons.R;
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter; import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
import java.util.Collections;
public class NearbyFilterSearchRecyclerViewAdapter public class NearbyFilterSearchRecyclerViewAdapter
extends RecyclerView.Adapter<NearbyFilterSearchRecyclerViewAdapter.RecyclerViewHolder> extends RecyclerView.Adapter<NearbyFilterSearchRecyclerViewAdapter.RecyclerViewHolder>

View file

@ -37,9 +37,9 @@ fun placeAdapterDelegate(
} }
cameraButton.setOnClickListener { onCameraClicked(item) } cameraButton.setOnClickListener { onCameraClicked(item) }
galleryButton.setOnClickListener { onGalleryClicked(item) } galleryButton.setOnClickListener { onGalleryClicked(item) }
bookmarkRowButton.setOnClickListener { bookmarkButtonImage.setOnClickListener {
val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item) val isBookmarked = bookmarkLocationDao.updateBookmarkLocation(item)
bookmarkRowButtonImage.setImageResource(if (isBookmarked) R.drawable.ic_round_star_filled_24px else R.drawable.ic_round_star_border_24px) bookmarkButtonImage.setImageResource(if (isBookmarked) R.drawable.ic_round_star_filled_24px else R.drawable.ic_round_star_border_24px)
onBookmarkClicked(item, isBookmarked) onBookmarkClicked(item, isBookmarked)
} }
iconOverflow.setOnClickListener { onOverflowIconClicked(item, it) } iconOverflow.setOnClickListener { onOverflowIconClicked(item, it) }
@ -59,7 +59,7 @@ fun placeAdapterDelegate(
if (item.hasCommonsLink() || item.hasWikidataLink()) VISIBLE if (item.hasCommonsLink() || item.hasWikidataLink()) VISIBLE
else GONE else GONE
bookmarkRowButtonImage.setImageResource( bookmarkButtonImage.setImageResource(
if (bookmarkLocationDao.findBookmarkLocation(item)) if (bookmarkLocationDao.findBookmarkLocation(item))
R.drawable.ic_round_star_filled_24px R.drawable.ic_round_star_filled_24px
else else

View file

@ -16,6 +16,7 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.Configuration; import android.content.res.Configuration;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.drawable.VectorDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.util.Log; import android.util.Log;
@ -33,6 +34,7 @@ import android.widget.RelativeLayout;
import android.widget.SearchView; import android.widget.SearchView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
@ -427,7 +429,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL); linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
recyclerView.setLayoutManager(linearLayoutManager); recyclerView.setLayoutManager(linearLayoutManager);
nearbyFilterSearchRecyclerViewAdapter = new NearbyFilterSearchRecyclerViewAdapter(getContext(),new ArrayList<>(TEXT_TO_DESCRIPTION.values()), recyclerView); nearbyFilterSearchRecyclerViewAdapter = new NearbyFilterSearchRecyclerViewAdapter(getContext(), new ArrayList<>(Label.valuesAsList()), recyclerView);
nearbyFilterSearchRecyclerViewAdapter.setCallback(new NearbyFilterSearchRecyclerViewAdapter.Callback() { nearbyFilterSearchRecyclerViewAdapter.setCallback(new NearbyFilterSearchRecyclerViewAdapter.Callback() {
@Override @Override
public void setCheckboxUnknown() { public void setCheckboxUnknown() {
@ -1128,7 +1130,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
// When label filter is engaged // When label filter is engaged
// then compare it against place's label // then compare it against place's label
if (selectedLabels != null && (selectedLabels.size() != 0 || !filterForPlaceState) if (selectedLabels != null && (selectedLabels.size() != 0 || !filterForPlaceState)
&& !selectedLabels.contains(place.getLabel())) { && (!selectedLabels.contains(place.getLabel())
&& !(selectedLabels.contains(Label.BOOKMARKS) && markerPlaceGroup.getIsBookmarked()))) {
continue; continue;
} }
@ -1168,25 +1171,10 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
public void updateMarker(final boolean isBookmarked, final Place place, @Nullable final fr.free.nrw.commons.location.LatLng curLatLng) { public void updateMarker(final boolean isBookmarked, final Place place, @Nullable final fr.free.nrw.commons.location.LatLng curLatLng) {
addPlaceToNearbyList(place); addPlaceToNearbyList(place);
final VectorDrawableCompat vectorDrawable; VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
if (isBookmarked) { getContext().getResources(), getIconFor(place, isBookmarked), getContext().getTheme());
vectorDrawable = VectorDrawableCompat.create(
getContext().getResources(), R.drawable.ic_custom_bookmark_marker, getContext().getTheme() for (Marker marker : mapBox.getMarkers()) {
);
} else if (!place.pic.trim().isEmpty()) {
vectorDrawable = VectorDrawableCompat.create( // Means place has picture
getContext().getResources(), R.drawable.ic_custom_map_marker_green, getContext().getTheme()
);
} else if (!place.destroyed.trim().isEmpty()) { // Means place is destroyed
vectorDrawable = VectorDrawableCompat.create( // Means place has picture
getContext().getResources(), R.drawable.ic_custom_map_marker_grey, getContext().getTheme()
);
} else {
vectorDrawable = VectorDrawableCompat.create(
getContext().getResources(), R.drawable.ic_custom_map_marker, getContext().getTheme()
);
}
for (final Marker marker : mapBox.getMarkers()) {
if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) { if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
final Bitmap icon = UiUtils.getBitmap(vectorDrawable); final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
@ -1209,6 +1197,22 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
} }
} }
private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) {
if (!place.pic.trim().isEmpty()) {
return (isBookmarked ?
R.drawable.ic_custom_map_marker_green_bookmarked :
R.drawable.ic_custom_map_marker_green);
} else if (!place.destroyed.trim().isEmpty()) { // Means place is destroyed
return (isBookmarked ?
R.drawable.ic_custom_map_marker_grey_bookmarked :
R.drawable.ic_custom_map_marker_grey);
} else {
return (isBookmarked ?
R.drawable.ic_custom_map_marker_blue_bookmarked :
R.drawable.ic_custom_map_marker);
}
}
/** /**
* Removes all markers except current location marker, an icon has been used * Removes all markers except current location marker, an icon has been used
* but it is transparent more than grey(as the name of the icon might suggest) * but it is transparent more than grey(as the name of the icon might suggest)

View file

@ -1,7 +1,9 @@
package fr.free.nrw.commons.nearby.presenter; package fr.free.nrw.commons.nearby.presenter;
import android.util.Log;
import android.view.View; import android.view.View;
import androidx.annotation.MainThread;
import com.mapbox.mapboxsdk.annotations.Marker; import com.mapbox.mapboxsdk.annotations.Marker;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
@ -301,6 +303,7 @@ public class NearbyParentFragmentPresenter
} }
@Override @Override
@MainThread
public void updateMapMarkersToController(List<NearbyBaseMarker> nearbyBaseMarkers) { public void updateMapMarkersToController(List<NearbyBaseMarker> nearbyBaseMarkers) {
NearbyController.markerExistsMap = new HashMap<>(); NearbyController.markerExistsMap = new HashMap<>();
NearbyController.markerNeedPicMap = new HashMap<>(); NearbyController.markerNeedPicMap = new HashMap<>();
@ -308,7 +311,7 @@ public class NearbyParentFragmentPresenter
for (int i = 0; i < nearbyBaseMarkers.size(); i++) { for (int i = 0; i < nearbyBaseMarkers.size(); i++) {
NearbyBaseMarker nearbyBaseMarker = nearbyBaseMarkers.get(i); NearbyBaseMarker nearbyBaseMarker = nearbyBaseMarkers.get(i);
NearbyController.markerLabelList.add( NearbyController.markerLabelList.add(
new MarkerPlaceGroup(nearbyBaseMarkers.get(i).getMarker(), bookmarkLocationDao.findBookmarkLocation(nearbyBaseMarkers.get(i).getPlace()), nearbyBaseMarker.getPlace())); new MarkerPlaceGroup(nearbyBaseMarker.getMarker(), bookmarkLocationDao.findBookmarkLocation(nearbyBaseMarker.getPlace()), nearbyBaseMarker.getPlace()));
//TODO: fix bookmark location //TODO: fix bookmark location
NearbyController.markerExistsMap.put((nearbyBaseMarkers.get(i).getPlace().hasWikidataLink()), nearbyBaseMarkers.get(i).getMarker()); NearbyController.markerExistsMap.put((nearbyBaseMarkers.get(i).getPlace().hasWikidataLink()), nearbyBaseMarkers.get(i).getMarker());
NearbyController.markerNeedPicMap.put(((nearbyBaseMarkers.get(i).getPlace().pic == null) ? true : false), nearbyBaseMarkers.get(i).getMarker()); NearbyController.markerNeedPicMap.put(((nearbyBaseMarkers.get(i).getPlace().pic == null) ? true : false), nearbyBaseMarkers.get(i).getMarker());

View file

@ -108,10 +108,12 @@ public class UploadRepository {
* *
* @param query * @param query
* @param imageTitleList * @param imageTitleList
* @param selectedDepictions
* @return * @return
*/ */
public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList) { public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList,
return categoriesModel.searchAll(query, imageTitleList); List<DepictedItem> selectedDepictions) {
return categoriesModel.searchAll(query, imageTitleList, selectedDepictions);
} }
/** /**

View file

@ -6,7 +6,6 @@ public class Prefs {
public static String TRACKING_ENABLED = "eventLogging"; public static String TRACKING_ENABLED = "eventLogging";
public static final String DEFAULT_LICENSE = "defaultLicense"; public static final String DEFAULT_LICENSE = "defaultLicense";
public static final String UPLOADS_SHOWING = "uploadsshowing"; public static final String UPLOADS_SHOWING = "uploadsshowing";
public static final String IS_CONTRIBUTION_COUNT_CHANGED = "ccontributionCountChanged";
public static final String MANAGED_EXIF_TAGS = "managed_exif_tags"; public static final String MANAGED_EXIF_TAGS = "managed_exif_tags";
public static final String KEY_LANGUAGE_VALUE = "languageDescription"; public static final String KEY_LANGUAGE_VALUE = "languageDescription";
public static final String KEY_THEME_VALUE = "appThemePref"; public static final String KEY_THEME_VALUE = "appThemePref";

View file

@ -65,43 +65,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}); });
} }
final EditTextPreference uploadLimit = findPreference("uploads");
int currentUploadLimit = defaultKvStore.getInt(Prefs.UPLOADS_SHOWING, 100);
uploadLimit.setText(String.valueOf(currentUploadLimit));
uploadLimit.setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue.toString().length() == 0) {
return false;
}
int value = Integer.parseInt(newValue.toString());
if (value > 500) {
Snackbar error = Snackbar.make(getView(), R.string.maximum_limit_alert, Snackbar.LENGTH_LONG);
error.show();
return false;
} else if (value == 0) {
Snackbar error = Snackbar.make(getView(), R.string.cannot_be_zero, Snackbar.LENGTH_LONG);
error.show();
return false;
}
return true;
});
uploadLimit.setOnBindEditTextListener(editText -> {
editText.setInputType(InputType.TYPE_CLASS_NUMBER);
editText.selectAll();
int maxLength = 3; // set maxLength to 3
editText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxLength)});
int value = Integer.parseInt(editText.getText().toString());
defaultKvStore.putInt(Prefs.UPLOADS_SHOWING, value);
defaultKvStore.putBoolean(Prefs.IS_CONTRIBUTION_COUNT_CHANGED, true);
uploadLimit.setText(Integer.toString(value));
});
langListPreference = findPreference("descriptionDefaultLanguagePref"); langListPreference = findPreference("descriptionDefaultLanguagePref");
prepareLanguages(); prepareLanguages();
Preference betaTesterPreference = findPreference("becomeBetaTester"); Preference betaTesterPreference = findPreference("becomeBetaTester");
@ -121,7 +84,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
findPreference("displayNearbyCardView").setEnabled(false); findPreference("displayNearbyCardView").setEnabled(false);
findPreference("displayLocationPermissionForCardView").setEnabled(false); findPreference("displayLocationPermissionForCardView").setEnabled(false);
findPreference("displayCampaignsCardView").setEnabled(false); findPreference("displayCampaignsCardView").setEnabled(false);
uploadLimit.setEnabled(false);
} }
} }

View file

@ -23,9 +23,14 @@ import fr.free.nrw.commons.di.CommonsDaggerService;
import fr.free.nrw.commons.media.MediaClient; import fr.free.nrw.commons.media.MediaClient;
import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.utils.CommonsDateUtil;
import fr.free.nrw.commons.wikidata.WikidataEditService; import fr.free.nrw.commons.wikidata.WikidataEditService;
import io.reactivex.Completable;
import io.reactivex.Observable; import io.reactivex.Observable;
import io.reactivex.Scheduler; import io.reactivex.Scheduler;
import io.reactivex.Single;
import io.reactivex.disposables.CompositeDisposable; import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Action;
import io.reactivex.functions.Consumer;
import io.reactivex.processors.PublishProcessor; import io.reactivex.processors.PublishProcessor;
import io.reactivex.schedulers.Schedulers; import io.reactivex.schedulers.Schedulers;
import java.io.File; import java.io.File;
@ -33,6 +38,7 @@ import java.io.IOException;
import java.text.ParseException; import java.text.ParseException;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.inject.Inject; import javax.inject.Inject;
@ -106,10 +112,10 @@ public class UploadService extends CommonsDaggerService {
notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build()); notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
contribution.setTransferred(transferred); contribution.setTransferred(transferred);
compositeDisposable.add(contributionDao.
save(contribution).subscribeOn(ioThreadScheduler) compositeDisposable.add(contributionDao.update(contribution)
.observeOn(mainThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe()); .subscribe());
} }
} }
@ -156,14 +162,11 @@ public class UploadService extends CommonsDaggerService {
Timber.d("%d uploads left", toUpload); Timber.d("%d uploads left", toUpload);
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build()); notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
} }
compositeDisposable.add(contributionDao compositeDisposable.add(contributionDao
.save(contribution) .save(contribution)
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler) .subscribe(() -> uploadContribution(contribution)));
.subscribe(aLong->{
contribution.set_id(aLong);
uploadContribution(contribution);
}, Throwable::printStackTrace));
} }
private boolean freshStart = true; private boolean freshStart = true;
@ -269,7 +272,7 @@ public class UploadService extends CommonsDaggerService {
} }
private void onUpload(Contribution contribution, String notificationTag, private void onUpload(Contribution contribution, String notificationTag,
UploadResult uploadResult) throws ParseException { UploadResult uploadResult) {
Timber.d("Stash upload response 2 is %s", uploadResult.toString()); Timber.d("Stash upload response 2 is %s", uploadResult.toString());
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS); notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
@ -282,8 +285,7 @@ public class UploadService extends CommonsDaggerService {
} }
} }
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) {
throws ParseException {
compositeDisposable compositeDisposable
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution)); .add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
WikidataPlace wikidataPlace = contribution.getWikidataPlace(); WikidataPlace wikidataPlace = contribution.getWikidataPlace();
@ -293,17 +295,11 @@ public class UploadService extends CommonsDaggerService {
saveCompletedContribution(contribution, uploadResult); saveCompletedContribution(contribution, uploadResult);
} }
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) throws ParseException { private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) {
contribution.setFilename(uploadResult.createCanonicalFileName()); compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename())
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl()); .map(media -> new Contribution(media, Contribution.STATE_COMPLETED))
contribution.setState(Contribution.STATE_COMPLETED); .flatMapCompletable(newContribution -> contributionDao.saveAndDelete(contribution, newContribution))
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp() .subscribe());
.parse(uploadResult.getImageinfo().getTimestamp()));
compositeDisposable.add(contributionDao
.save(contribution)
.subscribeOn(ioThreadScheduler)
.observeOn(mainThreadScheduler)
.subscribe());
} }
@SuppressLint("StringFormatInvalid") @SuppressLint("StringFormatInvalid")
@ -317,10 +313,11 @@ public class UploadService extends CommonsDaggerService {
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build()); notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build());
contribution.setState(Contribution.STATE_FAILED); contribution.setState(Contribution.STATE_FAILED);
compositeDisposable.add(contributionDao.save(contribution)
.subscribeOn(ioThreadScheduler) compositeDisposable.add(contributionDao
.observeOn(mainThreadScheduler) .update(contribution)
.subscribe()); .subscribeOn(ioThreadScheduler)
.subscribe());
} }
private String findUniqueFilename(String fileName) throws IOException { private String findUniqueFilename(String fileName) throws IOException {

View file

@ -60,7 +60,7 @@ class CategoriesPresenter @Inject constructor(
} }
private fun searchResults(term: String) = private fun searchResults(term: String) =
repository.searchAll(term, getImageTitleList()) repository.searchAll(term, getImageTitleList(), repository.selectedDepictions)
.subscribeOn(ioScheduler) .subscribeOn(ioScheduler)
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } } .map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }

View file

@ -24,5 +24,5 @@ public interface DepictsInterface {
Single<DepictSearchResponse> searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset); Single<DepictSearchResponse> searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset);
@GET("/w/api.php?format=json&action=wbgetentities") @GET("/w/api.php?format=json&action=wbgetentities")
Single<Entities> getEntities(@Query("ids")String ids, @Query("languages")String language); Single<Entities> getEntities(@Query("ids") String ids);
} }

View file

@ -107,7 +107,6 @@ class DepictsPresenter @Inject constructor(
view.noDepictionSelected() view.noDepictionSelected()
} }
} }
} }
/** /**

View file

@ -5,9 +5,11 @@ import fr.free.nrw.commons.explore.depictions.THUMB_IMAGE_SIZE
import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.upload.WikidataItem import fr.free.nrw.commons.upload.WikidataItem
import fr.free.nrw.commons.wikidata.WikidataProperties import fr.free.nrw.commons.wikidata.WikidataProperties
import fr.free.nrw.commons.wikidata.WikidataProperties.*
import org.wikipedia.wikidata.DataValue import org.wikipedia.wikidata.DataValue
import org.wikipedia.wikidata.Entities import org.wikipedia.wikidata.Entities
import org.wikipedia.wikidata.Statement_partial import org.wikipedia.wikidata.Statement_partial
import java.util.*
/** /**
* Model class for Depicted Item in Upload and Explore * Model class for Depicted Item in Upload and Explore
@ -17,14 +19,15 @@ data class DepictedItem constructor(
val description: String?, val description: String?,
val imageUrl: String?, val imageUrl: String?,
val instanceOfs: List<String>, val instanceOfs: List<String>,
val commonsCategories: List<String>,
var isSelected: Boolean, var isSelected: Boolean,
override val id: String override val id: String
) : WikidataItem { ) : WikidataItem {
constructor(entity: Entities.Entity) : this( constructor(entity: Entities.Entity) : this(
entity, entity,
entity.labels().values.firstOrNull()?.value() ?: "", entity.labels().byLanguageOrFirstOrEmpty(),
entity.descriptions().values.firstOrNull()?.value() ?: "" entity.descriptions().byLanguageOrFirstOrEmpty()
) )
constructor(entity: Entities.Entity, place: Place) : this( constructor(entity: Entities.Entity, place: Place) : this(
@ -36,10 +39,12 @@ data class DepictedItem constructor(
constructor(entity: Entities.Entity, name: String, description: String) : this( constructor(entity: Entities.Entity, name: String, description: String) : this(
name, name,
description, description,
entity[WikidataProperties.IMAGE].primaryImageValue?.let { entity[IMAGE].primaryImageValue?.let {
getImageUrl(it.value, THUMB_IMAGE_SIZE) getImageUrl(it.value, THUMB_IMAGE_SIZE)
}, },
entity[WikidataProperties.INSTANCE_OF].toIds(), entity[INSTANCE_OF].toIds(),
entity[COMMONS_CATEGORY]?.map { (it.mainSnak.dataValue as DataValue.ValueString).value }
?: emptyList(),
false, false,
entity.id() entity.id()
) )
@ -57,7 +62,7 @@ data class DepictedItem constructor(
} }
private fun List<Statement_partial>?.toIds(): List<String> { private fun List<Statement_partial>?.toIds(): List<String> {
return this?.map { it.mainSnak.dataValue } return this?.map { it.mainSnak.dataValue }
?.filterIsInstance<DataValue.EntityId>() ?.filterIsInstance<DataValue.EntityId>()
?.map { it.value.id } ?.map { it.value.id }
?: emptyList() ?: emptyList()
@ -69,3 +74,5 @@ private val List<Statement_partial>?.primaryImageValue: DataValue.ValueString?
operator fun Entities.Entity.get(property: WikidataProperties) = operator fun Entities.Entity.get(property: WikidataProperties) =
statements?.get(property.propertyName) statements?.get(property.propertyName)
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
let { it[Locale.getDefault().language] ?: it.values.firstOrNull() }?.value() ?: ""

View file

@ -21,6 +21,16 @@ public class CommonsDateUtil {
return simpleDateFormat; return simpleDateFormat;
} }
/**
* Gets SimpleDateFormat for date pattern returned by Media object
* @return simpledateformat
*/
public static SimpleDateFormat getMediaSimpleDateFormat() {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT);
simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return simpleDateFormat;
}
/** /**
* Gets the timestamp pattern for a date * Gets the timestamp pattern for a date
* @return timestamp * @return timestamp

View file

@ -3,6 +3,8 @@ package fr.free.nrw.commons.wikidata
import fr.free.nrw.commons.BuildConfig import fr.free.nrw.commons.BuildConfig
enum class WikidataProperties(val propertyName: String) { enum class WikidataProperties(val propertyName: String) {
IMAGE("P18"), DEPICTS(BuildConfig.DEPICTS_PROPERTY), INSTANCE_OF("P31"); IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY),
COMMONS_CATEGORY("P373"),
INSTANCE_OF("P31");
} }

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="28dp"
android:viewportWidth="24"
android:viewportHeight="28">
<path
android:pathData="M6.072,22.223a6.031,3.672 0,1 0,12.062 0a6.031,3.672 0,1 0,-12.062 0z"
android:strokeAlpha="0.1"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillAlpha="0.1"/>
<path
android:pathData="M11.575,11.62C10.689,11.462 9.902,10.759 9.625,9.878 9.553,9.65 9.535,9.499 9.538,9.14c0.004,-0.397 0.019,-0.492 0.13,-0.787 0.236,-0.631 0.646,-1.099 1.212,-1.382 0.386,-0.193 0.709,-0.272 1.116,-0.272 0.676,0 1.263,0.247 1.744,0.734 0.355,0.359 0.541,0.682 0.657,1.136 0.327,1.278 -0.442,2.611 -1.723,2.987 -0.282,0.083 -0.817,0.114 -1.099,0.063z"
android:strokeWidth="1"
android:fillColor="#00ff00"/>
<path
android:pathData="M11.617,21.707C10.518,20.424 9.338,18.864 8.395,17.449 6.524,14.641 5.455,12.305 5.102,10.255 5.014,9.744 5.006,8.628 5.088,8.137 5.348,6.561 6.043,5.221 7.158,4.148 9.148,2.231 12.016,1.668 14.593,2.688c2.043,0.809 3.607,2.581 4.162,4.719 0.174,0.67 0.204,0.933 0.203,1.761 -0.001,0.81 -0.035,1.098 -0.22,1.857 -0.614,2.524 -2.571,5.977 -5.383,9.501 -0.645,0.809 -1.321,1.61 -1.358,1.61 -0.008,0 -0.179,-0.193 -0.381,-0.428zM12.617,11.603c0.783,-0.188 1.457,-0.795 1.738,-1.564 0.516,-1.415 -0.317,-2.962 -1.783,-3.312 -0.216,-0.052 -0.317,-0.059 -0.661,-0.047 -0.354,0.012 -0.441,0.025 -0.682,0.104 -0.673,0.221 -1.205,0.695 -1.506,1.344 -0.176,0.38 -0.218,0.584 -0.217,1.054 0.001,0.324 0.014,0.452 0.064,0.635 0.266,0.97 1.077,1.689 2.079,1.844 0.243,0.038 0.68,0.012 0.968,-0.057z"
android:strokeWidth="1"
android:fillColor="#006699"
android:strokeColor="#003b59"
android:fillAlpha="1"/>
<path
android:pathData="M17.9025,7.0798 L14.1612,6.7552 12.7003,3.3154c-0.2628,-0.6261 -1.1595,-0.6261 -1.4223,0L9.817,6.7629 6.0835,7.0798C5.4032,7.134 5.125,7.9842 5.6429,8.4326l2.8369,2.4581 -0.8503,3.6485c-0.1546,0.6648 0.5643,1.1904 1.1518,0.8348l3.2079,-1.9325 3.2079,1.9402c0.5875,0.3556 1.3064,-0.1701 1.1518,-0.8348L15.4985,10.8907 18.3354,8.4326C18.8533,7.9842 18.5827,7.134 17.9025,7.0798Z"
android:strokeAlpha="1"
android:strokeWidth="1"
android:fillColor="#f84d4d"
android:fillAlpha="1"
android:strokeColor="#003b59"/>
</vector>

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="28dp"
android:viewportWidth="24"
android:viewportHeight="28">
<path
android:pathData="M6.072,22.223a6.031,3.672 0,1 0,12.062 0a6.031,3.672 0,1 0,-12.062 0z"
android:strokeAlpha="0.1"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillAlpha="0.1"/>
<path
android:pathData="M11.575,11.62C10.689,11.462 9.902,10.759 9.625,9.878 9.553,9.65 9.535,9.499 9.538,9.14c0.004,-0.397 0.019,-0.492 0.13,-0.787 0.236,-0.631 0.646,-1.099 1.212,-1.382 0.386,-0.193 0.709,-0.272 1.116,-0.272 0.676,0 1.263,0.247 1.744,0.734 0.355,0.359 0.541,0.682 0.657,1.136 0.327,1.278 -0.442,2.611 -1.723,2.987 -0.282,0.083 -0.817,0.114 -1.099,0.063z"
android:strokeWidth="1"
android:fillColor="#00ff00"/>
<path
android:pathData="M11.617,21.707C10.518,20.424 9.338,18.864 8.395,17.449 6.524,14.641 5.455,12.305 5.102,10.255 5.014,9.744 5.006,8.628 5.088,8.137 5.348,6.561 6.043,5.221 7.158,4.148 9.148,2.231 12.016,1.668 14.593,2.688c2.043,0.809 3.607,2.581 4.162,4.719 0.174,0.67 0.204,0.933 0.203,1.761 -0.001,0.81 -0.035,1.098 -0.22,1.857 -0.614,2.524 -2.571,5.977 -5.383,9.501 -0.645,0.809 -1.321,1.61 -1.358,1.61 -0.008,0 -0.179,-0.193 -0.381,-0.428zM12.617,11.603c0.783,-0.188 1.457,-0.795 1.738,-1.564 0.516,-1.415 -0.317,-2.962 -1.783,-3.312 -0.216,-0.052 -0.317,-0.059 -0.661,-0.047 -0.354,0.012 -0.441,0.025 -0.682,0.104 -0.673,0.221 -1.205,0.695 -1.506,1.344 -0.176,0.38 -0.218,0.584 -0.217,1.054 0.001,0.324 0.014,0.452 0.064,0.635 0.266,0.97 1.077,1.689 2.079,1.844 0.243,0.038 0.68,0.012 0.968,-0.057z"
android:strokeWidth="1"
android:fillColor="#4caf50"
android:strokeColor="#003b59"
android:fillAlpha="1"/>
<path
android:pathData="M17.9025,7.0798 L14.1612,6.7552 12.7003,3.3154c-0.2628,-0.6261 -1.1595,-0.6261 -1.4223,0L9.817,6.7629 6.0835,7.0798C5.4032,7.134 5.125,7.9842 5.6429,8.4326l2.8369,2.4581 -0.8503,3.6485c-0.1546,0.6648 0.5643,1.1904 1.1518,0.8348l3.2079,-1.9325 3.2079,1.9402c0.5875,0.3556 1.3064,-0.1701 1.1518,-0.8348L15.4985,10.8907 18.3354,8.4326C18.8533,7.9842 18.5827,7.134 17.9025,7.0798Z"
android:strokeAlpha="1"
android:strokeWidth="1"
android:fillColor="#f84d4d"
android:fillAlpha="1"
android:strokeColor="#003b59"/>
</vector>

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="28dp"
android:viewportWidth="24"
android:viewportHeight="28">
<path
android:pathData="M6.072,22.223a6.031,3.672 0,1 0,12.062 0a6.031,3.672 0,1 0,-12.062 0z"
android:strokeAlpha="0.1"
android:strokeWidth="1"
android:fillColor="#000000"
android:fillAlpha="0.1"/>
<path
android:pathData="M11.575,11.62C10.689,11.462 9.902,10.759 9.625,9.878 9.553,9.65 9.535,9.499 9.538,9.14c0.004,-0.397 0.019,-0.492 0.13,-0.787 0.236,-0.631 0.646,-1.099 1.212,-1.382 0.386,-0.193 0.709,-0.272 1.116,-0.272 0.676,0 1.263,0.247 1.744,0.734 0.355,0.359 0.541,0.682 0.657,1.136 0.327,1.278 -0.442,2.611 -1.723,2.987 -0.282,0.083 -0.817,0.114 -1.099,0.063z"
android:strokeWidth="1"
android:fillColor="#00ff00"/>
<path
android:pathData="M11.617,21.707C10.518,20.424 9.338,18.864 8.395,17.449 6.524,14.641 5.455,12.305 5.102,10.255 5.014,9.744 5.006,8.628 5.088,8.137 5.348,6.561 6.043,5.221 7.158,4.148 9.148,2.231 12.016,1.668 14.593,2.688c2.043,0.809 3.607,2.581 4.162,4.719 0.174,0.67 0.204,0.933 0.203,1.761 -0.001,0.81 -0.035,1.098 -0.22,1.857 -0.614,2.524 -2.571,5.977 -5.383,9.501 -0.645,0.809 -1.321,1.61 -1.358,1.61 -0.008,0 -0.179,-0.193 -0.381,-0.428zM12.617,11.603c0.783,-0.188 1.457,-0.795 1.738,-1.564 0.516,-1.415 -0.317,-2.962 -1.783,-3.312 -0.216,-0.052 -0.317,-0.059 -0.661,-0.047 -0.354,0.012 -0.441,0.025 -0.682,0.104 -0.673,0.221 -1.205,0.695 -1.506,1.344 -0.176,0.38 -0.218,0.584 -0.217,1.054 0.001,0.324 0.014,0.452 0.064,0.635 0.266,0.97 1.077,1.689 2.079,1.844 0.243,0.038 0.68,0.012 0.968,-0.057z"
android:strokeWidth="1"
android:fillColor="#454547"
android:strokeColor="#003b59"
android:fillAlpha="1"/>
<path
android:pathData="M17.9025,7.0798 L14.1612,6.7552 12.7003,3.3154c-0.2628,-0.6261 -1.1595,-0.6261 -1.4223,0L9.817,6.7629 6.0835,7.0798C5.4032,7.134 5.125,7.9842 5.6429,8.4326l2.8369,2.4581 -0.8503,3.6485c-0.1546,0.6648 0.5643,1.1904 1.1518,0.8348l3.2079,-1.9325 3.2079,1.9402c0.5875,0.3556 1.3064,-0.1701 1.1518,-0.8348L15.4985,10.8907 18.3354,8.4326C18.8533,7.9842 18.5827,7.134 17.9025,7.0798Z"
android:strokeAlpha="1"
android:strokeWidth="1"
android:fillColor="#f84d4d"
android:fillAlpha="1"
android:strokeColor="#003b59"/>
</vector>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingTop="@dimen/tiny_gap"
android:orientation="vertical">
<TextView
android:id="@+id/depictionNotFound"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/depictionsSearchResultsList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbarSize="@dimen/standard_gap"
android:scrollbars="vertical" />
<ProgressBar
android:id="@+id/depictionSearchInitialLoadProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"/>
</RelativeLayout>

View file

@ -6,13 +6,21 @@
android:focusableInTouchMode="true" android:focusableInTouchMode="true"
android:minHeight="@dimen/large_height"> android:minHeight="@dimen/large_height">
<ImageView
android:id="@+id/bookmarkButtonImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/standard_gap"
android:tint="?attr/rowButtonColor"
app:srcCompat="@drawable/ic_round_star_border_24px" />
<com.facebook.drawee.view.SimpleDraweeView <com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/icon" android:id="@+id/icon"
android:layout_width="@dimen/dimen_40" android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40" android:layout_height="@dimen/dimen_40"
android:layout_marginLeft="@dimen/standard_gap"
android:layout_marginStart="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap"
android:layout_marginTop="@dimen/standard_gap" android:layout_marginTop="@dimen/standard_gap"
android:layout_alignStart="@id/bookmarkButtonImage"
android:scaleType="centerCrop" android:scaleType="centerCrop"
tools:src="@drawable/empty_photo" tools:src="@drawable/empty_photo"
/> />

View file

@ -1,42 +1,14 @@
<LinearLayout <LinearLayout android:layout_width="match_parent"
android:layout_width="match_parent" android:layout_height="wrap_content"
android:layout_height="wrap_content" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/buttonLayout" android:id="@+id/buttonLayout"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal" android:orientation="horizontal"
android:visibility="gone" android:visibility="gone"
android:layout_marginTop="@dimen/standard_gap" tools:visibility="visible"
android:layout_below="@+id/icon" android:layout_marginTop="@dimen/standard_gap"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_below="@+id/icon"
> xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:id="@+id/bookmarkRowButton"
android:layout_width="@dimen/dimen_0"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/standard_gap"
android:clickable="true"
android:orientation="vertical"
android:background="@drawable/button_background_selector"
>
<ImageView
android:id="@+id/bookmarkRowButtonImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
app:srcCompat="@drawable/ic_round_star_border_24px"
android:tint="?attr/bookmarkButtonColor"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/bookmarkButtonText"
android:paddingTop="@dimen/activity_margin_horizontal"
android:layout_gravity="center_horizontal"
android:text="CAMERA"
android:visibility="gone"
/>
</LinearLayout>
<LinearLayout <LinearLayout
android:id="@+id/cameraButton" android:id="@+id/cameraButton"
@ -151,4 +123,4 @@
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -103,29 +103,28 @@
android:background="@color/divider_grey" /> android:background="@color/divider_grey" />
<Button <Button
android:id="@+id/depicts_next" android:id="@+id/depicts_next"
style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content"
android:layout_width="wrap_content" android:layout_height="wrap_content"
android:layout_height="wrap_content" android:layout_marginBottom="24dp"
android:layout_marginEnd="24dp" android:layout_marginEnd="24dp"
android:layout_marginRight="24dp" android:layout_marginRight="24dp"
android:layout_marginBottom="24dp" android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true" android:layout_alignParentEnd="true"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"
android:layout_alignParentBottom="true" android:text="@string/next" />
android:text="@string/next" />
<Button <Button
android:id="@+id/depicts_previous" android:id="@+id/depicts_previous"
style="@style/Widget.AppCompat.Button.Borderless" style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/standard_gap" android:layout_marginBottom="24dp"
android:layout_marginRight="@dimen/standard_gap" android:layout_marginEnd="@dimen/standard_gap"
android:layout_marginBottom="24dp" android:layout_marginRight="@dimen/standard_gap"
android:layout_toStartOf="@id/depicts_next" android:layout_alignParentBottom="true"
android:layout_toLeftOf="@id/depicts_next" android:layout_toLeftOf="@id/depicts_next"
android:layout_alignParentBottom="true" android:layout_toStartOf="@id/depicts_next"
android:text="@string/previous" /> android:text="@string/previous" />
</RelativeLayout> </RelativeLayout>

View file

@ -70,8 +70,10 @@
<string name="provider_contributions">بارگذاری‌های من</string> <string name="provider_contributions">بارگذاری‌های من</string>
<string name="menu_share">به اشتراک‌گذاشتن</string> <string name="menu_share">به اشتراک‌گذاشتن</string>
<string name="menu_open_in_browser">مشاهده در مرورگر</string> <string name="menu_open_in_browser">مشاهده در مرورگر</string>
<string name="share_title_hint" fuzzy="true">عنوان (مورد نیاز)</string> <string name="share_title_hint">شرح مختصر (الزامی)</string>
<string name="add_caption_toast">لطفاً شرح مختصری برای این پرونده بنویسید</string>
<string name="share_description_hint">توضیحات</string> <string name="share_description_hint">توضیحات</string>
<string name="share_caption_hint">شرح مختصر (محدود به ۲۵۵ نویسه)</string>
<string name="login_failed_network">قادر به ورود نیست - شکست شبکه‌ای</string> <string name="login_failed_network">قادر به ورود نیست - شکست شبکه‌ای</string>
<string name="login_failed_wrong_credentials">ورود به سیستم امکان پذیر نیست . لطفا نام کاربری و رمز عبور خود را بررسی کنید.</string> <string name="login_failed_wrong_credentials">ورود به سیستم امکان پذیر نیست . لطفا نام کاربری و رمز عبور خود را بررسی کنید.</string>
<string name="login_failed_throttled">تلاش ناموفق بیش از حد. لطفاً چند دقیقهٔ دیگر دوباره تلاش کنید</string> <string name="login_failed_throttled">تلاش ناموفق بیش از حد. لطفاً چند دقیقهٔ دیگر دوباره تلاش کنید</string>
@ -175,6 +177,7 @@
<string name="detail_panel_cats_label">رده‌ها</string> <string name="detail_panel_cats_label">رده‌ها</string>
<string name="detail_panel_cats_loading">در حال بارگیری…</string> <string name="detail_panel_cats_loading">در حال بارگیری…</string>
<string name="detail_panel_cats_none">هیچ‌کدام انتخاب‌نشده</string> <string name="detail_panel_cats_none">هیچ‌کدام انتخاب‌نشده</string>
<string name="detail_caption_empty">فاقد شرح</string>
<string name="detail_description_empty">بدون توضیحات</string> <string name="detail_description_empty">بدون توضیحات</string>
<string name="detail_discussion_empty">بدون بحث</string> <string name="detail_discussion_empty">بدون بحث</string>
<string name="detail_license_empty">مجوز ناشناخته</string> <string name="detail_license_empty">مجوز ناشناخته</string>
@ -193,6 +196,7 @@
<string name="upload">بارگذاری</string> <string name="upload">بارگذاری</string>
<string name="yes">بله</string> <string name="yes">بله</string>
<string name="no">خیر</string> <string name="no">خیر</string>
<string name="media_detail_caption">شرح مختصر</string>
<string name="media_detail_title">عنوان</string> <string name="media_detail_title">عنوان</string>
<string name="media_detail_description">توضیح</string> <string name="media_detail_description">توضیح</string>
<string name="media_detail_discussion">بحث</string> <string name="media_detail_discussion">بحث</string>
@ -255,6 +259,7 @@
<string name="error_while_cache">خطا در زمان دریافت تصاویر</string> <string name="error_while_cache">خطا در زمان دریافت تصاویر</string>
<string name="title_info">عنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، می‌توانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.</string> <string name="title_info">عنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، می‌توانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.</string>
<string name="description_info">لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شده‌است؟ شامل چه چیزی می‌شود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفه‌نظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر می‌رسد آن را شرح دهید.</string> <string name="description_info">لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شده‌است؟ شامل چه چیزی می‌شود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفه‌نظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر می‌رسد آن را شرح دهید.</string>
<string name="caption_info">لطفاً شرح مختصری برای این تصویر بنویسید. (محدود به ۲۵۵ نویسه)</string>
<string name="upload_image_too_dark">این تصویر خیلی تار است. آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری است که ارزش دانشنامه‌ای داشته باشند.</string> <string name="upload_image_too_dark">این تصویر خیلی تار است. آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری است که ارزش دانشنامه‌ای داشته باشند.</string>
<string name="upload_image_blurry">این تصویر خیلی محو است. آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری است که ارزش دانشنامه‌ای داشته باشند.</string> <string name="upload_image_blurry">این تصویر خیلی محو است. آیا مطمئنید که می‌خواهید آن را بارگذاری کنید؟ ویکی‌انبار فقط برای نگهداری از تصاویری است که ارزش دانشنامه‌ای داشته باشند.</string>
<string name="upload_problem_exist">مشکلات احتمالی با این تصویر :</string> <string name="upload_problem_exist">مشکلات احتمالی با این تصویر :</string>
@ -385,6 +390,7 @@
<string name="list_sheet">فهرست</string> <string name="list_sheet">فهرست</string>
<string name="storage_permission">اجازه ذخیره</string> <string name="storage_permission">اجازه ذخیره</string>
<string name="step_count">گام %1$d از %2$d</string> <string name="step_count">گام %1$d از %2$d</string>
<string name="image_in_set_label">تصویر %1$d در مجموعه</string>
<string name="next">بعدی</string> <string name="next">بعدی</string>
<string name="previous">قبلی</string> <string name="previous">قبلی</string>
<string name="submit">ارسال</string> <string name="submit">ارسال</string>
@ -473,6 +479,7 @@
<string name="share_via">اشتراک از طریق...</string> <string name="share_via">اشتراک از طریق...</string>
<string name="image_info">اطلاعات عکس</string> <string name="image_info">اطلاعات عکس</string>
<string name="no_categories_found">هیچ رده‌ای یافت نشد</string> <string name="no_categories_found">هیچ رده‌ای یافت نشد</string>
<string name="upload_cancelled">بارگذاری لغو شد</string>
<string name="delete_helper_show_deletion_title_success">موفق</string> <string name="delete_helper_show_deletion_title_success">موفق</string>
<string name="delete_helper_show_deletion_title_failed">ناموفق</string> <string name="delete_helper_show_deletion_title_failed">ناموفق</string>
<string name="delete_helper_ask_spam_selfie">یک سلفی</string> <string name="delete_helper_ask_spam_selfie">یک سلفی</string>

View file

@ -618,4 +618,5 @@
<string name="ask_to_turn_location_on">Activer la localisation?</string> <string name="ask_to_turn_location_on">Activer la localisation?</string>
<string name="nearby_needs_location">À proximité nécessite que la localisation soit activée pour bien fonctionner</string> <string name="nearby_needs_location">À proximité nécessite que la localisation soit activée pour bien fonctionner</string>
<string name="use_location_from_similar_image">Avez-vous pris ces deux photos du même endroit? Voulez-vous utiliser les latitude/longitude de limage sur la droite?</string> <string name="use_location_from_similar_image">Avez-vous pris ces deux photos du même endroit? Voulez-vous utiliser les latitude/longitude de limage sur la droite?</string>
<string name="nearby_no_results">Pas dendroit trouvé, essayez de modifier vos critères de recherche.</string>
</resources> </resources>

View file

@ -12,7 +12,7 @@
<string name="preference_category_privacy">Persónuvernd</string> <string name="preference_category_privacy">Persónuvernd</string>
<string name="preference_category_location">Staðsetning</string> <string name="preference_category_location">Staðsetning</string>
<string name="app_name">Commons</string> <string name="app_name">Commons</string>
<string name="bullet">• \</string> <string name="bullet">\\</string>
<string name="menu_settings">Stillingar</string> <string name="menu_settings">Stillingar</string>
<string name="intent_share_upload_label">Senda inn á Commons</string> <string name="intent_share_upload_label">Senda inn á Commons</string>
<string name="username">Notandanafn</string> <string name="username">Notandanafn</string>
@ -92,7 +92,7 @@
<string name="title_activity_category_details">Flokkur</string> <string name="title_activity_category_details">Flokkur</string>
<string name="title_activity_review">Yfirlestur jafningja</string> <string name="title_activity_review">Yfirlestur jafningja</string>
<string name="menu_about">Um</string> <string name="menu_about">Um</string>
<string name="about_license">Wikimedia Commons forritið er opinn og frjáls hugbúnaður sem gerður er og viðhaldið af stuðningsaðilum og sjálfboðaliðum Wikimedia samfélagsins. Wikimedia Foundation sjálfseignarstofnunin kemur ekki að gerð, forritun eða viðhaldi forritsins. \</string> <string name="about_license">Wikimedia Commons forritið er opinn og frjáls hugbúnaður sem gerður er og viðhaldið af stuðningsaðilum og sjálfboðaliðum Wikimedia samfélagsins. Wikimedia Foundation sjálfseignarstofnunin kemur ekki að gerð, forritun eða viðhaldi forritsins. \\</string>
<string name="about_improve">Útbúðu nýjar &lt;a href=\"%1$s\"&gt;GitHub tilkynningar (issue)&lt;/a&gt; til að koma villum og uppástungum á framfæri.</string> <string name="about_improve">Útbúðu nýjar &lt;a href=\"%1$s\"&gt;GitHub tilkynningar (issue)&lt;/a&gt; til að koma villum og uppástungum á framfæri.</string>
<string name="about_privacy_policy">Meðferð persónuupplýsinga</string> <string name="about_privacy_policy">Meðferð persónuupplýsinga</string>
<string name="about_credits">Framlög</string> <string name="about_credits">Framlög</string>
@ -322,7 +322,7 @@
<string name="wallpaper_set_successfully">Tókst að stilla bakgrunnsmynd!</string> <string name="wallpaper_set_successfully">Tókst að stilla bakgrunnsmynd!</string>
<string name="quiz">Spurningaleikur</string> <string name="quiz">Spurningaleikur</string>
<string name="quiz_question_string">Er í lagi að senda inn þessa mynd?</string> <string name="quiz_question_string">Er í lagi að senda inn þessa mynd?</string>
<string name="question">Spurning \</string> <string name="question">Spurning \\</string>
<string name="result">Niðurstaða</string> <string name="result">Niðurstaða</string>
<string name="quiz_back_button">Ef þú heldur áfram að senda inn myndir sem krefjast að þeim sé eytt, er líklegt að aðgangurinn þinn verði bannaður. Ertu viss um að þú viljir hætta í spurningaleiknum?</string> <string name="quiz_back_button">Ef þú heldur áfram að senda inn myndir sem krefjast að þeim sé eytt, er líklegt að aðgangurinn þinn verði bannaður. Ertu viss um að þú viljir hætta í spurningaleiknum?</string>
<string name="quiz_alert_message">Meira en %1$s myndanna sem þú hefur sent inn hefur verið eytt. Ef þú heldur áfram að senda inn myndir sem krefjast að þeim sé eytt, er líklegt að aðgangurinn þinn verði bannaður.\n\nMyndir þú vilja skða kennsluefnið aftur og taka síðan einn léttan spurningaleik til að hjálpa þér til að læra hvernig myndir þú ættir að senda inn og hverjar ekki?</string> <string name="quiz_alert_message">Meira en %1$s myndanna sem þú hefur sent inn hefur verið eytt. Ef þú heldur áfram að senda inn myndir sem krefjast að þeim sé eytt, er líklegt að aðgangurinn þinn verði bannaður.\n\nMyndir þú vilja skða kennsluefnið aftur og taka síðan einn léttan spurningaleik til að hjálpa þér til að læra hvernig myndir þú ættir að senda inn og hverjar ekki?</string>
@ -361,7 +361,7 @@
<string name="images_used_by_wiki">Myndir notaðar</string> <string name="images_used_by_wiki">Myndir notaðar</string>
<string name="achievements_share_message">Deildu frammistöðu þinni með vinum þínum!</string> <string name="achievements_share_message">Deildu frammistöðu þinni með vinum þínum!</string>
<string name="achievements_info_message">Stig þitt eykst eftir því sem þú uppfyllir betur þessar kröfur. Atriði í \"tölfræði\"-hlutanum teljast ekki með í stigaútreikningi þínum.</string> <string name="achievements_info_message">Stig þitt eykst eftir því sem þú uppfyllir betur þessar kröfur. Atriði í \"tölfræði\"-hlutanum teljast ekki með í stigaútreikningi þínum.</string>
<string name="achievements_revert_limit_message">lágmarksfjöldi sem er krafist: \</string> <string name="achievements_revert_limit_message">lágmarksfjöldi sem er krafist: \\</string>
<string name="images_uploaded_explanation">Heildarfjöldi mynda sem þú hefur sent inn á Commons, með hverskyns innsendingarhugbúnaði</string> <string name="images_uploaded_explanation">Heildarfjöldi mynda sem þú hefur sent inn á Commons, með hverskyns innsendingarhugbúnaði</string>
<string name="images_reverted_explanation">Prósentuhlutfall mynda sem þú hefur sent inn á Commons sem ekki hefur verið eytt</string> <string name="images_reverted_explanation">Prósentuhlutfall mynda sem þú hefur sent inn á Commons sem ekki hefur verið eytt</string>
<string name="images_used_explanation">Heildarfjöldi mynda sem þú hefur sent inn á Commons sem hafa verið notaðar með Wikimedia-greinum</string> <string name="images_used_explanation">Heildarfjöldi mynda sem þú hefur sent inn á Commons sem hafa verið notaðar með Wikimedia-greinum</string>

View file

@ -620,4 +620,5 @@
<string name="ask_to_turn_location_on">להפעיל מיקום?</string> <string name="ask_to_turn_location_on">להפעיל מיקום?</string>
<string name="nearby_needs_location">פעולת \"בסביבה\" זקוקה לשירותי מיקומי פועלים כדי לעבוד כמו שצריך</string> <string name="nearby_needs_location">פעולת \"בסביבה\" זקוקה לשירותי מיקומי פועלים כדי לעבוד כמו שצריך</string>
<string name="use_location_from_similar_image">האם צילמת את שתי התמונות באותו המקום? האם ברצונך להשתמש בקו הרוחב וקו האורך של התמונה משמאל?</string> <string name="use_location_from_similar_image">האם צילמת את שתי התמונות באותו המקום? האם ברצונך להשתמש בקו הרוחב וקו האורך של התמונה משמאל?</string>
<string name="nearby_no_results">לא נמצאו מקומות, נא לנסות לשנות את החיפוש.</string>
</resources> </resources>

View file

@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!-- Authors: <!-- Authors:
* Abijeet Patro * Abijeet Patro
* Diki Ananta
* N219
* NoiX180 * NoiX180
* Notanotheramy * Notanotheramy
--> -->
@ -15,11 +17,11 @@
<string name="bullet"></string> <string name="bullet"></string>
<string name="menu_settings">Setèlan</string> <string name="menu_settings">Setèlan</string>
<string name="intent_share_upload_label">Unggah menyang Commons</string> <string name="intent_share_upload_label">Unggah menyang Commons</string>
<string name="username">Jeneng panganggo</string> <string name="username">Jeneng naraguna</string>
<string name="password">Tembung wadi</string> <string name="password">Tembung sandi</string>
<string name="login_credential">Mlebu log akun Commons Beta panjenengan</string> <string name="login_credential">Mlebu log akun Commons Beta panjenengan</string>
<string name="login">Mlebu log</string> <string name="login">Mlebu log</string>
<string name="forgot_password">Lali Tembung Wadi?</string> <string name="forgot_password">Lali Tembung Sandi?</string>
<string name="signup">Dhaftar</string> <string name="signup">Dhaftar</string>
<string name="logging_in_title">Lagi mlebu log</string> <string name="logging_in_title">Lagi mlebu log</string>
<string name="logging_in_message">Entènana sadhela…</string> <string name="logging_in_message">Entènana sadhela…</string>
@ -53,9 +55,9 @@
<string name="share_title_hint">Sesirah (Kudu)</string> <string name="share_title_hint">Sesirah (Kudu)</string>
<string name="share_description_hint">Wedharan</string> <string name="share_description_hint">Wedharan</string>
<string name="login_failed_network">Ora bisa mlebu log - jaringané gagal</string> <string name="login_failed_network">Ora bisa mlebu log - jaringané gagal</string>
<string name="login_failed_wrong_credentials">Ora bisa mlebu log - priksa jeneng panganggo lan tembung wadi panjenengan</string> <string name="login_failed_wrong_credentials">Ora bisa mlebu log - priksa jeneng naraguna lan tembung sandi panjenengan</string>
<string name="login_failed_throttled">Kakèhan upaya sing gagal. Jajalana manèh mengko.</string> <string name="login_failed_throttled">Kakèhan upaya sing gagal. Jajalana manèh mengko.</string>
<string name="login_failed_blocked">Ngapunten, panganggo iki wis diblokir ing Commons</string> <string name="login_failed_blocked">Ngapunten, naraguna iki wis diblokir ing Commons</string>
<string name="login_failed_2fa_needed">Panjenengan kudu ngisi kodhe otèntifikasi rong faktoré panjenengan</string> <string name="login_failed_2fa_needed">Panjenengan kudu ngisi kodhe otèntifikasi rong faktoré panjenengan</string>
<string name="login_failed_generic">Wurung mlebu log</string> <string name="login_failed_generic">Wurung mlebu log</string>
<string name="share_upload_button">Unggah</string> <string name="share_upload_button">Unggah</string>

View file

@ -2,6 +2,7 @@
<!-- Authors: <!-- Authors:
* Abijeet Patro * Abijeet Patro
* CYAN * CYAN
* Codenstory
* Dlsrks1021 * Dlsrks1021
* Doyoon1995 * Doyoon1995
* Freebiekr * Freebiekr
@ -78,6 +79,7 @@
<string name="provider_modifications">바뀜</string> <string name="provider_modifications">바뀜</string>
<string name="menu_upload_single">올리기</string> <string name="menu_upload_single">올리기</string>
<string name="categories_search_text_hint">분류 검색</string> <string name="categories_search_text_hint">분류 검색</string>
<string name="depicts_search_text_hint">미디어가 서술한 항목을 검색하세요. (산, 타지마할 등)</string>
<string name="menu_save_categories">저장</string> <string name="menu_save_categories">저장</string>
<string name="refresh_button">새로 고침</string> <string name="refresh_button">새로 고침</string>
<string name="display_list_button">목록</string> <string name="display_list_button">목록</string>
@ -98,6 +100,9 @@
</plurals> </plurals>
<string name="categories_not_found">%1$s와(과) 일치하는 분류를 찾을 수 없습니다</string> <string name="categories_not_found">%1$s와(과) 일치하는 분류를 찾을 수 없습니다</string>
<string name="depictions_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="depictions_image_not_found">서술을 위한 이미지가 없습니다</string>
<string name="categories_skip_explanation">위키미디어 공용에서 그림을 더 찾기 쉽게 만들기 위해 분류를 추가합니다.\n분류를 추가하려면 입력을 시작하세요.</string> <string name="categories_skip_explanation">위키미디어 공용에서 그림을 더 찾기 쉽게 만들기 위해 분류를 추가합니다.\n분류를 추가하려면 입력을 시작하세요.</string>
<string name="categories_activity_title">분류</string> <string name="categories_activity_title">분류</string>
<string name="title_activity_settings">설정</string> <string name="title_activity_settings">설정</string>
@ -113,6 +118,7 @@
<string name="menu_feedback">(이메일로) 피드백 보내기</string> <string name="menu_feedback">(이메일로) 피드백 보내기</string>
<string name="no_email_client">설치된 이메일 클라이언트가 없습니다</string> <string name="no_email_client">설치된 이메일 클라이언트가 없습니다</string>
<string name="provider_categories">최근에 사용한 분류</string> <string name="provider_categories">최근에 사용한 분류</string>
<string name="provider_depictions">최근에 사용한 서술</string>
<string name="waiting_first_sync">첫 동기화를 기다리는 중…</string> <string name="waiting_first_sync">첫 동기화를 기다리는 중…</string>
<string name="no_uploads_yet">아직 사진을 올리지 않았습니다.</string> <string name="no_uploads_yet">아직 사진을 올리지 않았습니다.</string>
<string name="menu_retry_upload">다시 시도</string> <string name="menu_retry_upload">다시 시도</string>
@ -173,6 +179,7 @@
<string name="detail_panel_cats_loading">불러오는 중…</string> <string name="detail_panel_cats_loading">불러오는 중…</string>
<string name="detail_panel_cats_none">선택하지 않음</string> <string name="detail_panel_cats_none">선택하지 않음</string>
<string name="detail_caption_empty">설명 없음</string> <string name="detail_caption_empty">설명 없음</string>
<string name="detail_depiction_empty">서술 없음</string>
<string name="detail_description_empty">설명 없음</string> <string name="detail_description_empty">설명 없음</string>
<string name="detail_discussion_empty">토론 없음</string> <string name="detail_discussion_empty">토론 없음</string>
<string name="detail_license_empty">알 수 없는 라이선스</string> <string name="detail_license_empty">알 수 없는 라이선스</string>
@ -193,6 +200,7 @@
<string name="no">아니오</string> <string name="no">아니오</string>
<string name="media_detail_caption">설명</string> <string name="media_detail_caption">설명</string>
<string name="media_detail_title">제목</string> <string name="media_detail_title">제목</string>
<string name="media_detail_depiction">서술</string>
<string name="media_detail_description">설명</string> <string name="media_detail_description">설명</string>
<string name="media_detail_discussion">토론</string> <string name="media_detail_discussion">토론</string>
<string name="media_detail_author">저자</string> <string name="media_detail_author">저자</string>
@ -253,6 +261,7 @@
<string name="error_while_cache">그림 캐시 처리 오류</string> <string name="error_while_cache">그림 캐시 처리 오류</string>
<string name="title_info">이 파일을 설명할 수 있는 제목으로, 파일 이름으로 사용됩니다. 띄어쓰기를 포함한 일반적인 단어를 사용할 수 있습니다. 파일 확장자는 포함하지 마세요</string> <string name="title_info">이 파일을 설명할 수 있는 제목으로, 파일 이름으로 사용됩니다. 띄어쓰기를 포함한 일반적인 단어를 사용할 수 있습니다. 파일 확장자는 포함하지 마세요</string>
<string name="description_info">미디어에 대해 가능한 많이 설명하십시오: 어디서 촬영한 것인가? 무엇을 보여주는가? 무슨 문맥을 가지는가? 물건이나 사람에 대해 설명하십시오. 풍경에서 시간을 알려주는 것처럼 쉽게 추측할 수 없는 정보를 제공합니다. 미디어가 평범하지 않다면 무엇이 이를 평범하지 않게 만들었는지 설명하십시오.</string> <string name="description_info">미디어에 대해 가능한 많이 설명하십시오: 어디서 촬영한 것인가? 무엇을 보여주는가? 무슨 문맥을 가지는가? 물건이나 사람에 대해 설명하십시오. 풍경에서 시간을 알려주는 것처럼 쉽게 추측할 수 없는 정보를 제공합니다. 미디어가 평범하지 않다면 무엇이 이를 평범하지 않게 만들었는지 설명하십시오.</string>
<string name="caption_info">그림 설명을 작성해 주세요. (255자 제한)</string>
<string name="upload_image_too_dark">이 사진은 너무 어둡습니다. 정말 업로드하시겠습니까? 위키미디어 공용은 백과사전적인 가치가 있는 사진을 위한 공간입니다.</string> <string name="upload_image_too_dark">이 사진은 너무 어둡습니다. 정말 업로드하시겠습니까? 위키미디어 공용은 백과사전적인 가치가 있는 사진을 위한 공간입니다.</string>
<string name="upload_image_blurry">이 사진은 흐릿합니다. 정말 업로드하시겠습니까? 위키미디어 공용은 백과사전적인 가치가 있는 사진을 위한 공간입니다.</string> <string name="upload_image_blurry">이 사진은 흐릿합니다. 정말 업로드하시겠습니까? 위키미디어 공용은 백과사전적인 가치가 있는 사진을 위한 공간입니다.</string>
<string name="upload_problem_exist">이 그림에 잠재적인 문제가 있습니다:</string> <string name="upload_problem_exist">이 그림에 잠재적인 문제가 있습니다:</string>
@ -326,6 +335,7 @@
<string name="search_recent_header">최근 검색:</string> <string name="search_recent_header">최근 검색:</string>
<string name="provider_searches">최근 검색된 쿼리</string> <string name="provider_searches">최근 검색된 쿼리</string>
<string name="error_loading_categories">분류를 불러오는 동안 오류가 발생했습니다.</string> <string name="error_loading_categories">분류를 불러오는 동안 오류가 발생했습니다.</string>
<string name="error_loading_depictions">서술을 불러오는 동안 오류가 발생했습니다.</string>
<string name="error_loading_subcategories">하위 분류를 불러오는 동안 오류가 발생했습니다.</string> <string name="error_loading_subcategories">하위 분류를 불러오는 동안 오류가 발생했습니다.</string>
<string name="search_tab_title_media">미디어</string> <string name="search_tab_title_media">미디어</string>
<string name="search_tab_title_categories">분류</string> <string name="search_tab_title_categories">분류</string>
@ -426,7 +436,8 @@
<string name="desc_language_Asia">아시아</string> <string name="desc_language_Asia">아시아</string>
<string name="desc_language_Pacific">태평양</string> <string name="desc_language_Pacific">태평양</string>
<string name="no_categories_selected">선택된 분류 없음</string> <string name="no_categories_selected">선택된 분류 없음</string>
<string name="no_categories_selected_warning_desc" fuzzy="true">분류가 없는 그림은 거의 유용하지 않습니다. 분류를 선택하지 않고 제출하시겠습니까?</string> <string name="no_categories_selected_warning_desc">분류가 없는 그림은 거의 유용하지 않습니다. 분류를 선택하지 않고 제출하시겠습니까?</string>
<string name="no_depictions_selected">선택된 서술 없음</string>
<string name="search_this_area">여기를 검색</string> <string name="search_this_area">여기를 검색</string>
<string name="nearby_card_permission_title">권한 요청</string> <string name="nearby_card_permission_title">권한 요청</string>
<string name="nearby_card_permission_explanation">사진이 필요한 주변 장소를 표시하기 위해 현재 위치를 사용하시겠습니까?</string> <string name="nearby_card_permission_explanation">사진이 필요한 주변 장소를 표시하기 위해 현재 위치를 사용하시겠습니까?</string>
@ -477,6 +488,7 @@
<string name="share_via">앱 공유...</string> <string name="share_via">앱 공유...</string>
<string name="image_info">이미지 정보</string> <string name="image_info">이미지 정보</string>
<string name="no_categories_found">분류가 없습니다</string> <string name="no_categories_found">분류가 없습니다</string>
<string name="no_depiction_found">서술이 발견되지 않았습니다</string>
<string name="upload_cancelled">업로드 취소됨</string> <string name="upload_cancelled">업로드 취소됨</string>
<string name="default_description_language">기본 설명 언어</string> <string name="default_description_language">기본 설명 언어</string>
<string name="delete_helper_show_deletion_title_success">성공</string> <string name="delete_helper_show_deletion_title_success">성공</string>
@ -500,6 +512,8 @@
<string name="place_type">장소 유형:</string> <string name="place_type">장소 유형:</string>
<string name="nearby_search_hint">다리, 박물관, 호텔 등.</string> <string name="nearby_search_hint">다리, 박물관, 호텔 등.</string>
<string name="title_for_media">미디어</string> <string name="title_for_media">미디어</string>
<string name="title_for_child_classes">자식 클래스</string>
<string name="title_for_parent_classes">부모 클래스</string>
<string name="title_app_shortcut_explore">찾아보기</string> <string name="title_app_shortcut_explore">찾아보기</string>
<string name="title_app_shortcut_bookmark">북마크</string> <string name="title_app_shortcut_bookmark">북마크</string>
<string name="title_app_shortcut_setting">설정</string> <string name="title_app_shortcut_setting">설정</string>
@ -511,4 +525,5 @@
<string name="theme_dark_name">어두움</string> <string name="theme_dark_name">어두움</string>
<string name="theme_light_name">밝음</string> <string name="theme_light_name">밝음</string>
<string name="ask_to_turn_location_on">위치를 켭니까?</string> <string name="ask_to_turn_location_on">위치를 켭니까?</string>
<string name="nearby_no_results">발견된 장소가 없습니다. 검색 기준을 바꾸어 보십시오.</string>
</resources> </resources>

View file

@ -594,4 +594,5 @@
<string name="ask_to_turn_location_on">Да ја вклучам местоположбата?</string> <string name="ask_to_turn_location_on">Да ја вклучам местоположбата?</string>
<string name="nearby_needs_location">„Во близина“ бара местоположба за да работи</string> <string name="nearby_needs_location">„Во близина“ бара местоположба за да работи</string>
<string name="use_location_from_similar_image">Дали ги направивте ови две слики на истото место? Дали сакате да ја искористите географската ширина/должина од десната слика?</string> <string name="use_location_from_similar_image">Дали ги направивте ови две слики на истото место? Дали сакате да ја искористите географската ширина/должина од десната слика?</string>
<string name="nearby_no_results">Не пронајдов места. Изменете ги критериумите во барањето.</string>
</resources> </resources>

View file

@ -592,4 +592,5 @@
<string name="ask_to_turn_location_on">Anvisché la localisassion?</string> <string name="ask_to_turn_location_on">Anvisché la localisassion?</string>
<string name="nearby_needs_location">Nearby a l\'ha damanca dla localisassion abilità për marcé \'me ch\'as dev</string> <string name="nearby_needs_location">Nearby a l\'ha damanca dla localisassion abilità për marcé \'me ch\'as dev</string>
<string name="use_location_from_similar_image">A la fàit se doe fòto ant l\'istess pòst? A veul dovré la latitùdin e la longitùdin ëd la fòto an sla drita?</string> <string name="use_location_from_similar_image">A la fàit se doe fòto ant l\'istess pòst? A veul dovré la latitùdin e la longitùdin ëd la fòto an sla drita?</string>
<string name="nearby_no_results">Gnun pòst trovà, ch\'a preuva a modifiché ij sò criteri d\'arserca.</string>
</resources> </resources>

View file

@ -606,4 +606,5 @@
<string name="ask_to_turn_location_on">Ativar a localização?</string> <string name="ask_to_turn_location_on">Ativar a localização?</string>
<string name="nearby_needs_location">Proximidade precisa de localização ativado para funcionar corretamente</string> <string name="nearby_needs_location">Proximidade precisa de localização ativado para funcionar corretamente</string>
<string name="use_location_from_similar_image">Você tirou essas duas fotos no mesmo lugar? Deseja usar a latitude/longitude da imagem à direita?</string> <string name="use_location_from_similar_image">Você tirou essas duas fotos no mesmo lugar? Deseja usar a latitude/longitude da imagem à direita?</string>
<string name="nearby_no_results">Nenhum local encontrado, tente alterar seus critérios de pesquisa.</string>
</resources> </resources>

View file

@ -20,6 +20,7 @@
* Redredsonia * Redredsonia
* Rubin16 * Rubin16
* Vlad5250 * Vlad5250
* VoxelJ
* Wikisaurus * Wikisaurus
* Wirbel78 * Wirbel78
* Евгения * Евгения
@ -76,6 +77,7 @@
<string name="menu_share">Поделиться</string> <string name="menu_share">Поделиться</string>
<string name="menu_open_in_browser">Открыть в браузере</string> <string name="menu_open_in_browser">Открыть в браузере</string>
<string name="share_title_hint">Подпись (Обязательна)</string> <string name="share_title_hint">Подпись (Обязательна)</string>
<string name="add_caption_toast">Пожалуйста, укажите название этого файла</string>
<string name="share_description_hint">Описание</string> <string name="share_description_hint">Описание</string>
<string name="login_failed_network">Не удаётся войти — сбой сети</string> <string name="login_failed_network">Не удаётся войти — сбой сети</string>
<string name="login_failed_wrong_credentials">Не удаётся войти — проверьте ваше имя пользователя и пароль</string> <string name="login_failed_wrong_credentials">Не удаётся войти — проверьте ваше имя пользователя и пароль</string>
@ -537,6 +539,7 @@
<string name="images_featured_explanation">Избранные изображения обычно сделаны профессиональными фотографами и иллюстраторами. Такие изображения отмечены сообществом участников Викисклада как имеющие высшее качество на этом вебсайте.</string> <string name="images_featured_explanation">Избранные изображения обычно сделаны профессиональными фотографами и иллюстраторами. Такие изображения отмечены сообществом участников Викисклада как имеющие высшее качество на этом вебсайте.</string>
<string name="images_via_nearby_explanation">Изображения, загруженные участниками, которые находили места с помощью функционала \"Места поблизости\".</string> <string name="images_via_nearby_explanation">Изображения, загруженные участниками, которые находили места с помощью функционала \"Места поблизости\".</string>
<string name="thanks_received_explanation">Это функция позволяет участникам послать благодарность другим участникам за их полезные правки с помощью маленькой ссылки на странице истории страницы или странице разницы версий.</string> <string name="thanks_received_explanation">Это функция позволяет участникам послать благодарность другим участникам за их полезные правки с помощью маленькой ссылки на странице истории страницы или странице разницы версий.</string>
<string name="previous_image_caption_description">Скопировать предыдущие название и описание</string>
<string name="previous_button_tooltip_message">Нажмите, чтобы использовать название и описание, которые вы ввели для предыдущего изображения, а потом потом модифицировать их</string> <string name="previous_button_tooltip_message">Нажмите, чтобы использовать название и описание, которые вы ввели для предыдущего изображения, а потом потом модифицировать их</string>
<string name="welcome_do_upload_content_description">Примеры изображений, подходящих для загрузки на Викисклад</string> <string name="welcome_do_upload_content_description">Примеры изображений, подходящих для загрузки на Викисклад</string>
<string name="welcome_dont_upload_content_description">Примеры изображений, которые не следует загружать на Викисклад</string> <string name="welcome_dont_upload_content_description">Примеры изображений, которые не следует загружать на Викисклад</string>
@ -607,4 +610,5 @@
<string name="recommend_high_accuracy_mode">Для достижения наилучших результатов выберите режим высокой точности.</string> <string name="recommend_high_accuracy_mode">Для достижения наилучших результатов выберите режим высокой точности.</string>
<string name="ask_to_turn_location_on">Включить местоположение?</string> <string name="ask_to_turn_location_on">Включить местоположение?</string>
<string name="nearby_needs_location">Рядом необходимо, чтобы местоположение было включено для правильной работы.</string> <string name="nearby_needs_location">Рядом необходимо, чтобы местоположение было включено для правильной работы.</string>
<string name="nearby_no_results">Мест не найдено, попытайтесь изменить критерии поиска.</string>
</resources> </resources>

View file

@ -53,8 +53,10 @@
<string name="provider_contributions">Moje obrázky</string> <string name="provider_contributions">Moje obrázky</string>
<string name="menu_share">Zdieľať</string> <string name="menu_share">Zdieľať</string>
<string name="menu_open_in_browser">Otvoriť v prehliadači</string> <string name="menu_open_in_browser">Otvoriť v prehliadači</string>
<string name="share_title_hint" fuzzy="true">Názov (povinné)</string> <string name="share_title_hint">Popisok (povinné)</string>
<string name="add_caption_toast">Prosím, pridajte popisok pre tento súbor</string>
<string name="share_description_hint">Opis</string> <string name="share_description_hint">Opis</string>
<string name="share_caption_hint">Popisok (max. 255 znakov)</string>
<string name="login_failed_network">prihlásenie zlyhalo - zlyhanie siete</string> <string name="login_failed_network">prihlásenie zlyhalo - zlyhanie siete</string>
<string name="login_failed_wrong_credentials">Nie je možné sa prihlásiť skontrolujte, prosím, svoje používateľské meno a heslo</string> <string name="login_failed_wrong_credentials">Nie je možné sa prihlásiť skontrolujte, prosím, svoje používateľské meno a heslo</string>
<string name="login_failed_throttled">Príliš veľa neúspešných pokusov. Skúste to znova o niekoľko minút.</string> <string name="login_failed_throttled">Príliš veľa neúspešných pokusov. Skúste to znova o niekoľko minút.</string>
@ -67,6 +69,7 @@
<string name="provider_modifications">Úpravy</string> <string name="provider_modifications">Úpravy</string>
<string name="menu_upload_single">Nahrať</string> <string name="menu_upload_single">Nahrať</string>
<string name="categories_search_text_hint">Vyhľadávanie kategórií</string> <string name="categories_search_text_hint">Vyhľadávanie kategórií</string>
<string name="depicts_search_text_hint">Hľadať položky, ktoré váš multimediálny súbor zobrazuje (vrch, Tádž Mahal atď.)</string>
<string name="menu_save_categories">Uložiť</string> <string name="menu_save_categories">Uložiť</string>
<string name="refresh_button">Obnoviť</string> <string name="refresh_button">Obnoviť</string>
<string name="display_list_button">Zoznam</string> <string name="display_list_button">Zoznam</string>
@ -87,6 +90,7 @@
<item quantity="other">%1$d nahrania</item> <item quantity="other">%1$d nahrania</item>
</plurals> </plurals>
<string name="categories_not_found">Žiadne kategórie nezodpovedajú „%1$s“</string> <string name="categories_not_found">Žiadne kategórie nezodpovedajú „%1$s“</string>
<string name="depictions_not_found">Neboli nájdené žiadne položky Wikiúdajov zhodujúce sa s %1$s</string>
<string name="categories_skip_explanation">Pridajte kategórie, aby bolo vaše obrázky možné na Wikimedia Commons nájsť.\nPre pridanie kategórií začnite písať.</string> <string name="categories_skip_explanation">Pridajte kategórie, aby bolo vaše obrázky možné na Wikimedia Commons nájsť.\nPre pridanie kategórií začnite písať.</string>
<string name="categories_activity_title">Kategórie</string> <string name="categories_activity_title">Kategórie</string>
<string name="title_activity_settings">Nastavenia</string> <string name="title_activity_settings">Nastavenia</string>
@ -162,6 +166,7 @@
<string name="detail_panel_cats_label">Kategórie</string> <string name="detail_panel_cats_label">Kategórie</string>
<string name="detail_panel_cats_loading">Načítava sa…</string> <string name="detail_panel_cats_loading">Načítava sa…</string>
<string name="detail_panel_cats_none">Nič nebolo vybrané</string> <string name="detail_panel_cats_none">Nič nebolo vybrané</string>
<string name="detail_caption_empty">Žiadny titulok</string>
<string name="detail_description_empty">Bez popisu</string> <string name="detail_description_empty">Bez popisu</string>
<string name="detail_discussion_empty">Žiadna diskusia</string> <string name="detail_discussion_empty">Žiadna diskusia</string>
<string name="detail_license_empty">Neznáma licencia</string> <string name="detail_license_empty">Neznáma licencia</string>
@ -175,6 +180,7 @@
<string name="title_activity_nearby">Miesta v okolí</string> <string name="title_activity_nearby">Miesta v okolí</string>
<string name="no_nearby">V okolí sa nenašli žiadne miesta</string> <string name="no_nearby">V okolí sa nenašli žiadne miesta</string>
<string name="warning">Upozornenie</string> <string name="warning">Upozornenie</string>
<string name="duplicate_image_found">Nájdený duplicitný obrázok</string>
<string name="upload_image_duplicate">Tento súbor už na Commons existuje. Ste si istí, že chcete pokračovať?</string> <string name="upload_image_duplicate">Tento súbor už na Commons existuje. Ste si istí, že chcete pokračovať?</string>
<string name="upload">Nahrať</string> <string name="upload">Nahrať</string>
<string name="yes">Áno</string> <string name="yes">Áno</string>
@ -566,4 +572,5 @@
<string name="theme_default_name" fuzzy="true">Predvolený</string> <string name="theme_default_name" fuzzy="true">Predvolený</string>
<string name="theme_dark_name">Tmavý</string> <string name="theme_dark_name">Tmavý</string>
<string name="theme_light_name">Svetlý</string> <string name="theme_light_name">Svetlý</string>
<string name="ask_to_turn_location_on">Zapnúť lokáciu?</string>
</resources> </resources>

View file

@ -598,4 +598,5 @@
<string name="ask_to_turn_location_on">Aktivera plats?</string> <string name="ask_to_turn_location_on">Aktivera plats?</string>
<string name="nearby_needs_location">I närheten behöver ha plats aktiverat för att fungera ordentligt</string> <string name="nearby_needs_location">I närheten behöver ha plats aktiverat för att fungera ordentligt</string>
<string name="use_location_from_similar_image">Tog du dessa två bilder på samma plats? Vill du använda den högra bildens latitud/longitud?</string> <string name="use_location_from_similar_image">Tog du dessa två bilder på samma plats? Vill du använda den högra bildens latitud/longitud?</string>
<string name="nearby_no_results">Inga platser hittades, försök ändra dina sökkriterier.</string>
</resources> </resources>

View file

@ -610,4 +610,5 @@
<string name="ask_to_turn_location_on">Konum açılsın mı?</string> <string name="ask_to_turn_location_on">Konum açılsın mı?</string>
<string name="nearby_needs_location">Yakınımdakilerin düzgün çalışması için konumun açık olması gerekiyor</string> <string name="nearby_needs_location">Yakınımdakilerin düzgün çalışması için konumun açık olması gerekiyor</string>
<string name="use_location_from_similar_image">Bu iki fotoğrafı aynı yerde mi çektiniz? Sağdaki resmin enlem/boylamını kullanmak ister misiniz?</string> <string name="use_location_from_similar_image">Bu iki fotoğrafı aynı yerde mi çektiniz? Sağdaki resmin enlem/boylamını kullanmak ister misiniz?</string>
<string name="nearby_no_results">Yer bulunamadı, arama kriterlerinizi değiştirmeyi deneyin.</string>
</resources> </resources>

View file

@ -106,6 +106,9 @@
</plurals> </plurals>
<string name="categories_not_found">Категорій, відповідних %1$s, не знайдено</string> <string name="categories_not_found">Категорій, відповідних %1$s, не знайдено</string>
<string name="depictions_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="depictions_image_not_found">Немає зображення для описування</string>
<string name="categories_skip_explanation">Додайте категорії, щоб Ваші зображення було легше знайти у Вікісховищі. \n\nПочніть вводити текст, щоб додати категорії.</string> <string name="categories_skip_explanation">Додайте категорії, щоб Ваші зображення було легше знайти у Вікісховищі. \n\nПочніть вводити текст, щоб додати категорії.</string>
<string name="categories_activity_title">Категорії</string> <string name="categories_activity_title">Категорії</string>
<string name="title_activity_settings">Налаштування</string> <string name="title_activity_settings">Налаштування</string>
@ -206,6 +209,7 @@
<string name="no">Ні</string> <string name="no">Ні</string>
<string name="media_detail_caption">Підпис</string> <string name="media_detail_caption">Підпис</string>
<string name="media_detail_title">Назва</string> <string name="media_detail_title">Назва</string>
<string name="media_detail_depiction">Описи зображеного</string>
<string name="media_detail_description">Опис</string> <string name="media_detail_description">Опис</string>
<string name="media_detail_discussion">Обговорення</string> <string name="media_detail_discussion">Обговорення</string>
<string name="media_detail_author">Автор</string> <string name="media_detail_author">Автор</string>
@ -347,9 +351,11 @@
<string name="search_recent_header">Недавні пошуки:</string> <string name="search_recent_header">Недавні пошуки:</string>
<string name="provider_searches">Недавні пошукові запити</string> <string name="provider_searches">Недавні пошукові запити</string>
<string name="error_loading_categories">Сталася помилка під час завантаження категорій.</string> <string name="error_loading_categories">Сталася помилка під час завантаження категорій.</string>
<string name="error_loading_depictions">Сталася помилка при завантаженні описів зображеного.</string>
<string name="error_loading_subcategories">Сталася помилка під час завантаження підкатегорій.</string> <string name="error_loading_subcategories">Сталася помилка під час завантаження підкатегорій.</string>
<string name="search_tab_title_media">Медіафайли</string> <string name="search_tab_title_media">Медіафайли</string>
<string name="search_tab_title_categories">Категорії</string> <string name="search_tab_title_categories">Категорії</string>
<string name="search_tab_title_depictions">Елементи</string>
<string name="explore_tab_title_featured">Обране</string> <string name="explore_tab_title_featured">Обране</string>
<string name="explore_tab_title_mobile">Завантаження з мобільного</string> <string name="explore_tab_title_mobile">Завантаження з мобільного</string>
<string name="successful_wikidata_edit">Зображення додано до сторінки %1$s у Вікіданих!</string> <string name="successful_wikidata_edit">Зображення додано до сторінки %1$s у Вікіданих!</string>
@ -455,6 +461,7 @@
<string name="desc_language_Pacific">Океанія</string> <string name="desc_language_Pacific">Океанія</string>
<string name="no_categories_selected">Жодної категорії не вибрано</string> <string name="no_categories_selected">Жодної категорії не вибрано</string>
<string name="no_categories_selected_warning_desc">Зображення без категорій рідко використовуються. Ви впевнені, що хочете продовжити без вказаних категорій?</string> <string name="no_categories_selected_warning_desc">Зображення без категорій рідко використовуються. Ви впевнені, що хочете продовжити без вказаних категорій?</string>
<string name="no_depictions_selected">Описів зображеного не вибрано</string>
<string name="no_depictions_selected_warning_desc">Медіа, у яких вказані зображувані об\'єкти, можуть бути легше знайдені та використані надалі. Ви впевнені, що хочете продовжити не вказавши що саме тут зображено?</string> <string name="no_depictions_selected_warning_desc">Медіа, у яких вказані зображувані об\'єкти, можуть бути легше знайдені та використані надалі. Ви впевнені, що хочете продовжити не вказавши що саме тут зображено?</string>
<string name="upload_flow_all_images_in_set">(Для всіх зображень у наборі)</string> <string name="upload_flow_all_images_in_set">(Для всіх зображень у наборі)</string>
<string name="search_this_area">Шукати в цій зоні</string> <string name="search_this_area">Шукати в цій зоні</string>
@ -555,6 +562,7 @@
<string name="share_via">Поділитися програмкою через…</string> <string name="share_via">Поділитися програмкою через…</string>
<string name="image_info">Інформація про зображення</string> <string name="image_info">Інформація про зображення</string>
<string name="no_categories_found">Категорії не знайдені</string> <string name="no_categories_found">Категорії не знайдені</string>
<string name="no_depiction_found">Описів зображеного не знайдено</string>
<string name="upload_cancelled">Завантаження скасовано</string> <string name="upload_cancelled">Завантаження скасовано</string>
<string name="previous_image_title_description_not_found">Відсутній заголовок або опис попереднього зображення</string> <string name="previous_image_title_description_not_found">Відсутній заголовок або опис попереднього зображення</string>
<string name="dialog_box_text_nomination">Чому %1$s має бути видалено?</string> <string name="dialog_box_text_nomination">Чому %1$s має бути видалено?</string>
@ -587,6 +595,9 @@
<string name="place_type">Тип місця:</string> <string name="place_type">Тип місця:</string>
<string name="nearby_search_hint">Міст, музей, готель тощо</string> <string name="nearby_search_hint">Міст, музей, готель тощо</string>
<string name="you_must_reset_your_passsword">Щось пішло не так із процесом входу, ви маєте скинути пароль !</string> <string name="you_must_reset_your_passsword">Щось пішло не так із процесом входу, ви маєте скинути пароль !</string>
<string name="title_for_media">МЕДІА</string>
<string name="title_for_child_classes">ДОЧІРНІ КЛАСИ</string>
<string name="title_for_parent_classes">БАТЬКІВСЬКІ КЛАСИ</string>
<string name="upload_nearby_place_found_title">Знайдено місце поблизу</string> <string name="upload_nearby_place_found_title">Знайдено місце поблизу</string>
<string name="upload_nearby_place_found_description">Чи це — фото місця %1$s?</string> <string name="upload_nearby_place_found_description">Чи це — фото місця %1$s?</string>
<string name="title_app_shortcut_explore">Дослідити</string> <string name="title_app_shortcut_explore">Дослідити</string>
@ -605,4 +616,5 @@
<string name="ask_to_turn_location_on">Увімкнути визначення місця розташування?</string> <string name="ask_to_turn_location_on">Увімкнути визначення місця розташування?</string>
<string name="nearby_needs_location">«Поблизу» потребує увімкненого визначення місця розташування, щоб працювати належним чином</string> <string name="nearby_needs_location">«Поблизу» потребує увімкненого визначення місця розташування, щоб працювати належним чином</string>
<string name="use_location_from_similar_image">Ви зробили ці два знімки в одному й тому ж місці? Хочете використати широту/довготу зображення справа?</string> <string name="use_location_from_similar_image">Ви зробили ці два знімки в одному й тому ж місці? Хочете використати широту/довготу зображення справа?</string>
<string name="nearby_no_results">Місць не знайдено, спробуйте змінити критерії пошуку.</string>
</resources> </resources>

View file

@ -603,4 +603,5 @@
<string name="ask_to_turn_location_on">打開位置?</string> <string name="ask_to_turn_location_on">打開位置?</string>
<string name="nearby_needs_location">附近功能需要啟用位置才能運作正常</string> <string name="nearby_needs_location">附近功能需要啟用位置才能運作正常</string>
<string name="use_location_from_similar_image">您是否在同一地點拍攝了這兩張圖片?您要使用圖片右側的緯度/經度嗎?</string> <string name="use_location_from_similar_image">您是否在同一地點拍攝了這兩張圖片?您要使用圖片右側的緯度/經度嗎?</string>
<string name="nearby_no_results">查無地點,請嘗試更改您的搜尋條件。</string>
</resources> </resources>

View file

@ -20,6 +20,7 @@
* ZhaoGang * ZhaoGang
* 予弦 * 予弦
* 佛壁灯 * 佛壁灯
* 列维劳德
* 夢蝶葬花 * 夢蝶葬花
* 沈澄心 * 沈澄心
* 神樂坂秀吉 * 神樂坂秀吉
@ -74,6 +75,7 @@
<string name="menu_share">分享</string> <string name="menu_share">分享</string>
<string name="menu_open_in_browser">在浏览器中查看</string> <string name="menu_open_in_browser">在浏览器中查看</string>
<string name="share_title_hint" fuzzy="true">标题 (要求)</string> <string name="share_title_hint" fuzzy="true">标题 (要求)</string>
<string name="add_caption_toast">请提供此文件的描述</string>
<string name="share_description_hint">描述</string> <string name="share_description_hint">描述</string>
<string name="login_failed_network">无法登录 - 网络故障</string> <string name="login_failed_network">无法登录 - 网络故障</string>
<string name="login_failed_wrong_credentials">无法登录——请检查您的用户名和密码</string> <string name="login_failed_wrong_credentials">无法登录——请检查您的用户名和密码</string>

View file

@ -26,13 +26,6 @@
android:summary="@string/use_external_storage_summary" android:summary="@string/use_external_storage_summary"
android:title="@string/use_external_storage" /> android:title="@string/use_external_storage" />
<EditTextPreference
android:defaultValue="100"
android:key="uploads"
app:useSimpleSummaryProvider="true"
app:singleLineTitle="false"
android:title="@string/set_limit" />
<ListPreference <ListPreference
android:key="descriptionDefaultLanguagePref" android:key="descriptionDefaultLanguagePref"
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"

View file

@ -0,0 +1,23 @@
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
fun depictedItem(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
commonsCategories: List<String> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(
name = name,
description = description,
imageUrl = imageUrl,
instanceOfs = instanceOfs,
commonsCategories = commonsCategories,
isSelected = isSelected,
id = id
)
fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)

View file

@ -35,7 +35,9 @@ class BookMarkLocationDaoTest {
COLUMN_WIKIDATA_LINK, COLUMN_WIKIDATA_LINK,
COLUMN_COMMONS_LINK, COLUMN_COMMONS_LINK,
COLUMN_LAT, COLUMN_LAT,
COLUMN_LONG) COLUMN_LONG,
COLUMN_PIC,
COLUMN_DESTROYED)
private val client: ContentProviderClient = mock() private val client: ContentProviderClient = mock()
private val database: SQLiteDatabase = mock() private val database: SQLiteDatabase = mock()
private val captor = argumentCaptor<ContentValues>() private val captor = argumentCaptor<ContentValues>()
@ -93,6 +95,8 @@ class BookMarkLocationDaoTest {
assertEquals(builder.build().wikipediaLink, it.siteLinks.wikipediaLink) assertEquals(builder.build().wikipediaLink, it.siteLinks.wikipediaLink)
assertEquals(builder.build().wikidataLink, it.siteLinks.wikidataLink) assertEquals(builder.build().wikidataLink, it.siteLinks.wikidataLink)
assertEquals(builder.build().commonsLink, it.siteLinks.commonsLink) assertEquals(builder.build().commonsLink, it.siteLinks.commonsLink)
assertEquals("picName",it.pic)
assertEquals("placeDestroyed", it.destroyed)
} }
} }
} }
@ -145,7 +149,7 @@ class BookMarkLocationDaoTest {
assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark)) assertTrue(testObject.updateBookmarkLocation(examplePlaceBookmark))
verify(client).insert(eq(BASE_URI), captor.capture()) verify(client).insert(eq(BASE_URI), captor.capture())
captor.firstValue.let { cv -> captor.firstValue.let { cv ->
assertEquals(11, cv.size()) assertEquals(12, cv.size())
assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME)) assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME))
assertEquals(examplePlaceBookmark.longDescription, cv.getAsString(COLUMN_DESCRIPTION)) assertEquals(examplePlaceBookmark.longDescription, cv.getAsString(COLUMN_DESCRIPTION))
assertEquals(examplePlaceBookmark.label.text, cv.getAsString(COLUMN_LABEL_TEXT)) assertEquals(examplePlaceBookmark.label.text, cv.getAsString(COLUMN_LABEL_TEXT))
@ -156,6 +160,7 @@ class BookMarkLocationDaoTest {
assertEquals(examplePlaceBookmark.siteLinks.wikidataLink.toString(), cv.getAsString(COLUMN_WIKIDATA_LINK)) assertEquals(examplePlaceBookmark.siteLinks.wikidataLink.toString(), cv.getAsString(COLUMN_WIKIDATA_LINK))
assertEquals(examplePlaceBookmark.siteLinks.commonsLink.toString(), cv.getAsString(COLUMN_COMMONS_LINK)) assertEquals(examplePlaceBookmark.siteLinks.commonsLink.toString(), cv.getAsString(COLUMN_COMMONS_LINK))
assertEquals(examplePlaceBookmark.pic.toString(), cv.getAsString(COLUMN_PIC)) assertEquals(examplePlaceBookmark.pic.toString(), cv.getAsString(COLUMN_PIC))
assertEquals(examplePlaceBookmark.destroyed.toString(), cv.getAsString(COLUMN_DESTROYED))
} }
} }
@ -251,12 +256,18 @@ class BookMarkLocationDaoTest {
verify(database).execSQL(CREATE_TABLE_STATEMENT) verify(database).execSQL(CREATE_TABLE_STATEMENT)
} }
@Test
fun migrateTableVersionFrom_v12_to_v13() {
onUpdate(database, 12, 13)
verify(database).execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;")
}
private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply { private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply {
for (i in 0 until rowCount) { for (i in 0 until rowCount) {
addRow(listOf("placeName", "placeDescription","placeCategory", exampleLabel.text, exampleLabel.icon, addRow(listOf("placeName", "placeDescription","placeCategory", exampleLabel.text, exampleLabel.icon,
exampleUri, builder.build().wikipediaLink, builder.build().wikidataLink, builder.build().commonsLink, exampleUri, builder.build().wikipediaLink, builder.build().wikidataLink, builder.build().commonsLink,
exampleLocation.latitude, exampleLocation.longitude)) exampleLocation.latitude, exampleLocation.longitude, "picName", "placeDestroyed"))
} }
} }
} }

View file

@ -1,7 +1,10 @@
package fr.free.nrw.commons.category package fr.free.nrw.commons.category
import categoryItem
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.upload.GpsCategoryModel import fr.free.nrw.commons.upload.GpsCategoryModel
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
@ -36,11 +39,11 @@ class CategoriesModelTest {
// Checking if both return "Test" // Checking if both return "Test"
val expectedItems = expectedList.map { CategoryItem(it, false) } val expectedItems = expectedList.map { CategoryItem(it, false) }
categoriesModel.searchAll("tes", emptyList()) categoriesModel.searchAll("tes", emptyList(), emptyList())
.test() .test()
.assertValues(expectedItems) .assertValues(expectedItems)
categoriesModel.searchAll("Tes", emptyList()) categoriesModel.searchAll("Tes", emptyList(), emptyList())
.test() .test()
.assertValues(expectedItems) .assertValues(expectedItems)
} }
@ -48,6 +51,7 @@ class CategoriesModelTest {
@Test @Test
fun `searchAll with empty search terms creates results from gps, title search & recents`() { fun `searchAll with empty search terms creates results from gps, title search & recents`() {
val gpsCategoryModel: GpsCategoryModel = mock() val gpsCategoryModel: GpsCategoryModel = mock()
val depictedItem = depictedItem(commonsCategories = listOf("depictionCategory"))
whenever(gpsCategoryModel.categoriesFromLocation) whenever(gpsCategoryModel.categoriesFromLocation)
.thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory"))) .thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory")))
@ -55,13 +59,14 @@ class CategoriesModelTest {
.thenReturn(Observable.just(listOf("titleSearch"))) .thenReturn(Observable.just(listOf("titleSearch")))
whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories")) whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories"))
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
.searchAll("", listOf("tes")) .searchAll("", listOf("tes"), listOf(depictedItem))
.test() .test()
.assertValue( .assertValue(
listOf( listOf(
CategoryItem("gpsCategory", false), categoryItem("depictionCategory"),
CategoryItem("titleSearch", false), categoryItem("gpsCategory"),
CategoryItem("recentCategories", false) categoryItem("titleSearch"),
categoryItem("recentCategories")
) )
) )
} }

View file

@ -0,0 +1,139 @@
package fr.free.nrw.commons.contributions
import android.content.Context
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.utils.NetworkUtilsTest
import fr.free.nrw.commons.utils.createMockDataSourceFactory
import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import io.reactivex.schedulers.TestScheduler
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers.*
import org.mockito.InjectMocks
import org.mockito.Mock
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
import java.lang.RuntimeException
import java.util.*
/**
* The unit test class for ContributionBoundaryCallbackTest
*/
class ContributionBoundaryCallbackTest {
@Mock
internal lateinit var repository: ContributionsRepository
@Mock
internal lateinit var sessionManager: SessionManager
@Mock
internal lateinit var mediaClient: MediaClient
private lateinit var contributionBoundaryCallback: ContributionBoundaryCallback
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var scheduler: Scheduler
/**
* initial setup
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
scheduler = Schedulers.trampoline()
contributionBoundaryCallback =
ContributionBoundaryCallback(repository, sessionManager, mediaClient, scheduler);
}
@Test
fun testOnZeroItemsLoaded() {
whenever(repository.save(anyList<Contribution>()))
.thenReturn(Single.just(listOf(1L, 2L)))
whenever(sessionManager.userName).thenReturn("Test")
whenever(mediaClient.getMediaListForUser(anyString())).thenReturn(
Single.just(listOf(mock(Media::class.java)))
)
whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString()))
.thenReturn(true)
contributionBoundaryCallback.onZeroItemsLoaded()
verify(repository).save(anyList<Contribution>());
verify(mediaClient).getMediaListForUser(anyString());
}
@Test
fun testOnLastItemLoaded() {
whenever(repository.save(anyList<Contribution>()))
.thenReturn(Single.just(listOf(1L, 2L)))
whenever(sessionManager.userName).thenReturn("Test")
whenever(mediaClient.getMediaListForUser(anyString())).thenReturn(
Single.just(listOf(mock(Media::class.java)))
)
whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString()))
.thenReturn(true)
contributionBoundaryCallback.onItemAtEndLoaded(mock(Contribution::class.java))
verify(repository).save(anyList());
verify(mediaClient).getMediaListForUser(anyString());
}
@Test
fun testOnFrontItemLoaded() {
whenever(repository.save(anyList<Contribution>()))
.thenReturn(Single.just(listOf(1L, 2L)))
whenever(sessionManager.userName).thenReturn("Test")
whenever(mediaClient.getMediaListForUser(anyString())).thenReturn(
Single.just(listOf(mock(Media::class.java)))
)
whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString()))
.thenReturn(true)
contributionBoundaryCallback.onItemAtFrontLoaded(mock(Contribution::class.java))
verify(repository).save(anyList());
verify(mediaClient).getMediaListForUser(anyString());
}
@Test
fun testFetchContributions() {
whenever(repository.save(anyList<Contribution>()))
.thenReturn(Single.just(listOf(1L, 2L)))
whenever(sessionManager.userName).thenReturn("Test")
whenever(mediaClient.getMediaListForUser(anyString())).thenReturn(
Single.just(listOf(mock(Media::class.java)))
)
whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString()))
.thenReturn(true)
contributionBoundaryCallback.fetchContributions()
verify(repository).save(anyList());
verify(mediaClient).getMediaListForUser(anyString());
}
@Test
fun testFetchContributionsForEndOfList() {
whenever(sessionManager.userName).thenReturn("Test")
whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString()))
.thenReturn(false)
contributionBoundaryCallback.fetchContributions()
verify(mediaClient, times(0)).getMediaListForUser(anyString())
verifyNoMoreInteractions(repository)
}
@Test
fun testFetchContributionsFailed() {
whenever(sessionManager.userName).thenReturn("Test")
whenever(mediaClient.doesMediaListForUserHaveMorePages(anyString()))
.thenReturn(true)
whenever(mediaClient.getMediaListForUser(anyString())).thenReturn(Single.error(Exception("Error")))
contributionBoundaryCallback.fetchContributions()
verifyZeroInteractions(repository);
verify(mediaClient).getMediaListForUser(anyString());
}
}

View file

@ -0,0 +1,63 @@
package fr.free.nrw.commons.contributions
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Completable
import io.reactivex.Scheduler
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.any
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.Mockito.mock
import org.mockito.MockitoAnnotations
/**
* The unit test class for ContributionsListPresenterTest
*/
class ContributionsListPresenterTest {
@Mock
internal lateinit var contributionBoundaryCallback: ContributionBoundaryCallback
@Mock
internal lateinit var repository: ContributionsRepository
@Rule
@JvmField
var instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var scheduler: Scheduler
lateinit var contributionsListPresenter: ContributionsListPresenter
/**
* initial setup
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
scheduler = Schedulers.trampoline()
contributionsListPresenter =
ContributionsListPresenter(contributionBoundaryCallback, repository, scheduler);
}
@Test
fun testDeleteUpload() {
whenever(repository.deleteContributionFromDB(any<Contribution>()))
.thenReturn(Completable.complete())
contributionsListPresenter.deleteUpload(mock(Contribution::class.java))
verify(repository, times(1))
.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java));
}
}

View file

@ -7,9 +7,11 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.loader.content.CursorLoader import androidx.loader.content.CursorLoader
import androidx.loader.content.Loader import androidx.loader.content.Loader
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import io.reactivex.Completable
import io.reactivex.Scheduler import io.reactivex.Scheduler
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
@ -17,9 +19,11 @@ import org.junit.Before
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers
import org.mockito.ArgumentMatchers.*
import org.mockito.Mock import org.mockito.Mock
import org.mockito.Mockito import org.mockito.Mockito
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import java.util.concurrent.TimeUnit
/** /**
* The unit test class for ContributionsPresenter * The unit test class for ContributionsPresenter
@ -42,7 +46,7 @@ class ContributionsPresenterTest {
@Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule() @Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var scheduler : Scheduler lateinit var scheduler : TestScheduler
/** /**
* initial setup * initial setup
@ -60,23 +64,13 @@ class ContributionsPresenterTest {
liveData=MutableLiveData() liveData=MutableLiveData()
} }
/**
* Test fetch contributions
*/
@Test
fun testFetchContributions(){
whenever(repository.getString(ArgumentMatchers.anyString())).thenReturn("10")
whenever(repository.fetchContributions()).thenReturn(liveData)
contributionsPresenter.fetchContributions()
verify(repository).fetchContributions()
}
/** /**
* Test presenter actions onDeleteContribution * Test presenter actions onDeleteContribution
*/ */
@Test @Test
fun testDeleteContribution() { fun testDeleteContribution() {
whenever(repository.deleteContributionFromDB(ArgumentMatchers.any(Contribution::class.java))).thenReturn(Single.just(1)) whenever(repository.deleteContributionFromDB(ArgumentMatchers.any<Contribution>()))
.thenReturn(Completable.complete())
contributionsPresenter.deleteUpload(contribution) contributionsPresenter.deleteUpload(contribution)
verify(repository).deleteContributionFromDB(contribution) verify(repository).deleteContributionFromDB(contribution)
} }

View file

@ -0,0 +1,55 @@
package fr.free.nrw.commons.contributions
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.utils.createMockDataSourceFactory
import io.reactivex.Scheduler
import io.reactivex.Single
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.*
import org.mockito.Mockito.any
import org.mockito.Mockito.mock
/**
* The unit test class for ContributionsRepositoryTest
*/
class ContributionsRepositoryTest {
@Mock
internal lateinit var localDataSource: ContributionsLocalDataSource
@InjectMocks
private lateinit var contributionsRepository: ContributionsRepository
lateinit var scheduler: Scheduler
/**
* initial setup
*/
@Before
@Throws(Exception::class)
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun testFetchContributions() {
val contribution = mock(Contribution::class.java)
whenever(localDataSource.getContributions())
.thenReturn(createMockDataSourceFactory(listOf(contribution)))
val contributionsFactory = contributionsRepository.fetchContributions()
verify(localDataSource, times(1)).getContributions();
}
@Test
fun testSaveContribution() {
val contributions = listOf(mock(Contribution::class.java))
whenever(localDataSource.saveContributions(ArgumentMatchers.anyList()))
.thenReturn(Single.just(listOf(1L)))
val save = contributionsRepository.save(contributions).test().assertValueAt(0) {
it.size == 1 && it.get(0) == 1L
}
}
}

View file

@ -0,0 +1,53 @@
package fr.free.nrw.commons.explore.depictions
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.spy
import com.nhaarman.mockitokotlin2.verify
import io.reactivex.processors.PublishProcessor
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.`is`
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
class SearchDepictionsDataSourceFactoryTest {
@Mock
private lateinit var depictsClient: DepictsClient
@Mock
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var factory: SearchDepictionsDataSourceFactory
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
factory = SearchDepictionsDataSourceFactory(depictsClient, "test", loadingStates)
}
@Test
fun `create returns a dataSource`() {
assertThat(
factory.create(),
`is`(SearchDepictionsDataSource(depictsClient, loadingStates, "test"))
)
}
@Test
@Ignore("Rewrite with Mockk constructor mocks")
fun `retryFailedRequest invokes method if not null`() {
val spyFactory = spy(factory)
val dataSource = mock<SearchDepictionsDataSource>()
Mockito.doReturn(dataSource).`when`(spyFactory).create()
factory.retryFailedRequest()
verify(dataSource).retryFailedRequest()
}
@Test
fun `retryFailedRequest does not invoke method if null`() {
factory.retryFailedRequest()
}
}

View file

@ -0,0 +1,106 @@
package fr.free.nrw.commons.explore.depictions
import androidx.paging.PositionalDataSource
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.explore.depictions.LoadingState.*
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.Single
import io.reactivex.plugins.RxJavaPlugins
import io.reactivex.processors.PublishProcessor
import io.reactivex.schedulers.Schedulers
import org.junit.After
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchDepictionsDataSourceTest {
@Mock
private lateinit var depictsClient: DepictsClient
private lateinit var loadingStates: PublishProcessor<LoadingState>
private lateinit var searchDepictionsDataSource: SearchDepictionsDataSource
@Before
fun setUp() {
RxJavaPlugins.setIoSchedulerHandler { Schedulers.trampoline() }
MockitoAnnotations.initMocks(this)
loadingStates = PublishProcessor.create()
searchDepictionsDataSource =
SearchDepictionsDataSource(depictsClient, loadingStates, "test")
}
@After
fun tearDown() {
RxJavaPlugins.reset()
}
@Test
fun `loadInitial returns results and emits InitialLoad & Complete`() {
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
val callback = mock<PositionalDataSource.LoadInitialCallback<DepictedItem>>()
whenever(depictsClient.searchForDepictions("test", 1, 0))
.thenReturn(Single.just(emptyList()))
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadInitial(params, callback)
verify(callback).onResult(emptyList(), 0)
testSubscriber.assertValues(InitialLoad, Complete)
}
@Test
fun `loadInitial onError does not return results and emits InitialLoad & Error`() {
val params = PositionalDataSource.LoadInitialParams(0, 1, 2, false)
val callback = mock<PositionalDataSource.LoadInitialCallback<DepictedItem>>()
whenever(depictsClient.searchForDepictions("test", 1, 0))
.thenThrow(RuntimeException())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadInitial(params, callback)
verify(callback, never()).onResult(any(), any())
testSubscriber.assertValues(InitialLoad, Error)
}
@Test
fun `loadRange returns results and emits Loading & Complete`() {
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
.thenReturn(Single.just(emptyList()))
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback).onResult(emptyList())
testSubscriber.assertValues(Loading, Complete)
}
@Test
fun `loadRange onError does not return results and emits Loading & Error`() {
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
.thenThrow(RuntimeException())
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback, never()).onResult(any())
testSubscriber.assertValues(Loading, Error)
}
@Test
fun `retryFailedRequest does nothing when null`() {
searchDepictionsDataSource.retryFailedRequest()
verifyNoMoreInteractions(depictsClient)
}
@Test
fun `retryFailedRequest retries last request`() {
val callback: PositionalDataSource.LoadRangeCallback<DepictedItem> = mock()
val params = PositionalDataSource.LoadRangeParams(0, 1)
whenever(depictsClient.searchForDepictions("test", params.loadSize, params.startPosition))
.thenThrow(RuntimeException()).thenReturn(Single.just(emptyList()))
val testSubscriber = loadingStates.test()
searchDepictionsDataSource.loadRange(params, callback)
verify(callback, never()).onResult(any())
searchDepictionsDataSource.retryFailedRequest()
verify(callback).onResult(emptyList())
testSubscriber.assertValues(Loading, Error, Loading, Complete)
}
}

View file

@ -0,0 +1,80 @@
package fr.free.nrw.commons.explore.depictions
import androidx.lifecycle.LiveData
import androidx.paging.PagedList
import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import io.reactivex.processors.PublishProcessor
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.MockitoAnnotations
class SearchableDepictionsDataSourceFactoryTest {
@Mock
private lateinit var searchDepictionsDataSourceFactoryFactory: SearchDepictionsDataSourceFactoryFactory
@Mock
private lateinit var liveDataConverter: LiveDataConverter
private lateinit var factory: SearchableDepictionsDataSourceFactory
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
factory = SearchableDepictionsDataSourceFactory(
searchDepictionsDataSourceFactoryFactory,
liveDataConverter
)
}
@Test
fun `onQueryUpdated emits new liveData`() {
val (_, liveData) = expectNewLiveData()
factory.searchResults.test()
.also { factory.onQueryUpdated("test") }
.assertValue(liveData)
}
@Test
fun `onQueryUpdated invokes livedatconverter with no items emitter`() {
val (captor, _) = expectNewLiveData()
factory.onQueryUpdated("test")
factory.noItemsLoadedEvent.test()
.also { captor.firstValue.invoke() }
.assertValue(Unit)
}
/*
* Just for coverage, no way to really assert this
* */
@Test
fun `retryFailedRequest does nothing without a factory`() {
factory.retryFailedRequest()
}
@Test
fun `retryFailedRequest retries with a factory`() {
val (_, _, dataSourceFactory) = expectNewLiveData()
factory.onQueryUpdated("test")
factory.retryFailedRequest()
verify(dataSourceFactory).retryFailedRequest()
}
private fun expectNewLiveData(): Triple<KArgumentCaptor<() -> Unit>, LiveData<PagedList<DepictedItem>>, SearchDepictionsDataSourceFactory> {
val dataSourceFactory: SearchDepictionsDataSourceFactory = mock()
whenever(
searchDepictionsDataSourceFactoryFactory.create(
"test",
factory.loadingStates as PublishProcessor<LoadingState>
)
).thenReturn(dataSourceFactory)
val captor = argumentCaptor<() -> Unit>()
val liveData: LiveData<PagedList<DepictedItem>> = mock()
whenever(liveDataConverter.convert(eq(dataSourceFactory), captor.capture()))
.thenReturn(liveData)
return Triple(captor, liveData, dataSourceFactory)
}
}

View file

@ -1,5 +1,6 @@
package fr.free.nrw.commons.media package fr.free.nrw.commons.media
import com.nhaarman.mockitokotlin2.whenever
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.utils.CommonsDateUtil import fr.free.nrw.commons.utils.CommonsDateUtil
import io.reactivex.Observable import io.reactivex.Observable
@ -7,18 +8,16 @@ import junit.framework.Assert.*
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import org.mockito.* import org.mockito.*
import org.mockito.Mockito.`when`
import org.mockito.Mockito.mock
import org.wikipedia.dataclient.mwapi.ImageDetails import org.wikipedia.dataclient.mwapi.ImageDetails
import org.wikipedia.dataclient.mwapi.MwQueryPage import org.wikipedia.dataclient.mwapi.MwQueryPage
import org.wikipedia.dataclient.mwapi.MwQueryResponse import org.wikipedia.dataclient.mwapi.MwQueryResponse
import org.wikipedia.dataclient.mwapi.MwQueryResult import org.wikipedia.dataclient.mwapi.MwQueryResult
import org.wikipedia.gallery.ImageInfo import org.wikipedia.gallery.ImageInfo
import org.mockito.ArgumentCaptor import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatchers.*
import java.util.* import java.util.*
import org.mockito.Captor import org.mockito.Captor
import org.mockito.Mockito.*
class MediaClientTest { class MediaClientTest {
@ -46,9 +45,10 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
val checkPageExistsUsingTitle = mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() val checkPageExistsUsingTitle =
mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet()
assertTrue(checkPageExistsUsingTitle) assertTrue(checkPageExistsUsingTitle)
} }
@ -63,9 +63,10 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkPageExistsUsingTitle(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
val checkPageExistsUsingTitle = mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet() val checkPageExistsUsingTitle =
mediaClient!!.checkPageExistsUsingTitle("File:Test.jpg").blockingGet()
assertFalse(checkPageExistsUsingTitle) assertFalse(checkPageExistsUsingTitle)
} }
@ -80,7 +81,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet() val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
assertTrue(checkFileExistsUsingSha) assertTrue(checkFileExistsUsingSha)
@ -97,7 +98,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString())) `when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet() val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
assertFalse(checkFileExistsUsingSha) assertFalse(checkFileExistsUsingSha)
@ -117,7 +118,7 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
assertEquals("Test", mediaClient!!.getMedia("abcde").blockingGet().filename) assertEquals("Test", mediaClient!!.getMedia("abcde").blockingGet().filename)
} }
@ -136,10 +137,11 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
assertEquals(Media.EMPTY, mediaClient!!.getMedia("abcde").blockingGet()) assertEquals(Media.EMPTY, mediaClient!!.getMedia("abcde").blockingGet())
} }
@Captor @Captor
private val filenameCaptor: ArgumentCaptor<String>? = null private val filenameCaptor: ArgumentCaptor<String>? = null
@ -159,18 +161,18 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture())) `when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
assertEquals("Test", mediaClient!!.getPictureOfTheDay().blockingGet().filename) assertEquals("Test", mediaClient!!.getPictureOfTheDay().blockingGet().filename)
assertEquals(template, filenameCaptor.value); assertEquals(template, filenameCaptor.value);
} }
@Captor @Captor
private val continuationCaptor: ArgumentCaptor<Map<String, String>>? = null private val continuationCaptor: ArgumentCaptor<Map<String, String>>? = null
@Test @Test
fun getMediaListFromCategoryTwice() { fun getMediaListFromCategoryTwice() {
val mockContinuation= mapOf(Pair("gcmcontinue", "test")) val mockContinuation = mapOf(Pair("gcmcontinue", "test"))
val imageInfo = ImageInfo() val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java) val mwQueryPage = mock(MwQueryPage::class.java)
@ -184,9 +186,13 @@ class MediaClientTest {
`when`(mockResponse.query()).thenReturn(mwQueryResult) `when`(mockResponse.query()).thenReturn(mwQueryResult)
`when`(mockResponse.continuation()).thenReturn(mockContinuation) `when`(mockResponse.continuation()).thenReturn(mockContinuation)
`when`(mediaInterface!!.getMediaListFromCategory(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(), `when`(
continuationCaptor!!.capture())) mediaInterface!!.getMediaListFromCategory(
.thenReturn(Observable.just(mockResponse)) ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(),
continuationCaptor!!.capture()
)
)
.thenReturn(Observable.just(mockResponse))
val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0) val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0) val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
@ -197,6 +203,38 @@ class MediaClientTest {
assertEquals(media2.filename, "Test") assertEquals(media2.filename, "Test")
} }
@Test
fun getMediaListForUser() {
val mockContinuation = mapOf("gcmcontinue" to "test")
val imageInfo = ImageInfo()
val mwQueryPage = mock(MwQueryPage::class.java)
whenever(mwQueryPage.title()).thenReturn("Test")
whenever(mwQueryPage.imageInfo()).thenReturn(imageInfo)
val mwQueryResult = mock(MwQueryResult::class.java)
whenever(mwQueryResult.pages()).thenReturn(listOf(mwQueryPage))
val mockResponse = mock(MwQueryResponse::class.java)
whenever(mockResponse.query()).thenReturn(mwQueryResult)
whenever(mockResponse.continuation()).thenReturn(mockContinuation)
whenever(
mediaInterface!!.getMediaListForUser(
ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(),
continuationCaptor!!.capture()
)
)
.thenReturn(Observable.just(mockResponse))
val media1 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0)
val media2 = mediaClient!!.getMediaListForUser("Test").blockingGet().get(0)
verify(mediaInterface, times(2))?.getMediaListForUser(
ArgumentMatchers.anyString(),
ArgumentMatchers.anyInt(), ArgumentMatchers.anyMap<String, String>()
)
}
@Test @Test
fun getPageHtmlTest() { fun getPageHtmlTest() {
val mwParseResult = mock(MwParseResult::class.java) val mwParseResult = mock(MwParseResult::class.java)
@ -207,7 +245,7 @@ class MediaClientTest {
mockResponse.setParse(mwParseResult) mockResponse.setParse(mwParseResult)
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet()) assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet())
} }
@ -218,7 +256,7 @@ class MediaClientTest {
mockResponse.setParse(null) mockResponse.setParse(null)
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString())) `when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
.thenReturn(Observable.just(mockResponse)) .thenReturn(Observable.just(mockResponse))
assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet()) assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet())
} }

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.upload package fr.free.nrw.commons.upload
import categoryItem
import com.nhaarman.mockitokotlin2.* import com.nhaarman.mockitokotlin2.*
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.categories.CategoriesContract import fr.free.nrw.commons.upload.categories.CategoriesContract
import fr.free.nrw.commons.upload.categories.CategoriesPresenter import fr.free.nrw.commons.upload.categories.CategoriesPresenter
@ -27,11 +27,6 @@ class CategoriesPresenterTest {
private lateinit var testScheduler: TestScheduler private lateinit var testScheduler: TestScheduler
private val categoryItems: ArrayList<CategoryItem> = ArrayList()
@Mock
lateinit var categoryItem: CategoryItem
/** /**
* initial setup * initial setup
*/ */
@ -40,7 +35,6 @@ class CategoriesPresenterTest {
fun setUp() { fun setUp() {
MockitoAnnotations.initMocks(this) MockitoAnnotations.initMocks(this)
testScheduler = TestScheduler() testScheduler = TestScheduler()
categoryItems.add(categoryItem)
categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler) categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler)
categoriesPresenter.onAttachView(view) categoriesPresenter.onAttachView(view)
} }
@ -62,7 +56,7 @@ class CategoriesPresenterTest {
emptyCaptionUploadItem emptyCaptionUploadItem
) )
) )
whenever(repository.searchAll("test", listOf("nonEmpty"))) whenever(repository.searchAll("test", listOf("nonEmpty"), repository.selectedDepictions))
.thenReturn( .thenReturn(
Observable.just( Observable.just(
listOf( listOf(
@ -87,7 +81,7 @@ class CategoriesPresenterTest {
@Test @Test
fun `searchForCategoriesTest sets Error when list is empty`() { fun `searchForCategoriesTest sets Error when list is empty`() {
whenever(repository.uploads).thenReturn(listOf()) whenever(repository.uploads).thenReturn(listOf())
whenever(repository.searchAll(any(), any())).thenReturn(Observable.just(listOf())) whenever(repository.searchAll(any(), any(), any())).thenReturn(Observable.just(listOf()))
whenever(repository.selectedCategories).thenReturn(listOf()) whenever(repository.selectedCategories).thenReturn(listOf())
categoriesPresenter.searchForCategories("test") categoriesPresenter.searchForCategories("test")
testScheduler.triggerActions() testScheduler.triggerActions()
@ -124,10 +118,8 @@ class CategoriesPresenterTest {
*/ */
@Test @Test
fun onCategoryItemClickedTest() { fun onCategoryItemClickedTest() {
val categoryItem = categoryItem()
categoriesPresenter.onCategoryItemClicked(categoryItem) categoriesPresenter.onCategoryItemClicked(categoryItem)
verify(repository).onCategoryClicked(categoryItem) verify(repository).onCategoryClicked(categoryItem)
} }
private fun categoryItem(name: String = "name", selected: Boolean = false) =
CategoryItem(name, selected)
} }

View file

@ -4,11 +4,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.jraska.livedata.test import com.jraska.livedata.test
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import depictedItem
import fr.free.nrw.commons.explore.depictions.DepictsClient import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.repository.UploadRepository
import fr.free.nrw.commons.upload.depicts.DepictsContract import fr.free.nrw.commons.upload.depicts.DepictsContract
import fr.free.nrw.commons.upload.depicts.DepictsPresenter import fr.free.nrw.commons.upload.depicts.DepictsPresenter
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems
import io.reactivex.Flowable import io.reactivex.Flowable
import io.reactivex.schedulers.TestScheduler import io.reactivex.schedulers.TestScheduler
@ -62,8 +62,8 @@ class DepictsPresenterTest {
depictedItem(id="nonUnique"), depictedItem(id="nonUnique"),
depictedItem(id="nonUnique"), depictedItem(id="nonUnique"),
depictedItem( depictedItem(
id = "unique", instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id),
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id) id = "unique"
) )
) )
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults)) whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
@ -78,6 +78,7 @@ class DepictsPresenterTest {
.assertValue(listOf(selectedItem, depictedItem(id="nonUnique"))) .assertValue(listOf(selectedItem, depictedItem(id="nonUnique")))
} }
@Test @Test
fun `empty search results with empty term do not show error`() { fun `empty search results with empty term do not show error`() {
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(emptyList())) whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(emptyList()))
@ -137,15 +138,4 @@ class DepictsPresenterTest {
depictsPresenter.verifyDepictions() depictsPresenter.verifyDepictions()
verify(view).noDepictionSelected() verify(view).noDepictionSelected()
} }
} }
fun depictedItem(
name: String = "label",
description: String = "desc",
imageUrl: String = "",
instanceOfs: List<String> = listOf(),
isSelected: Boolean = false,
id: String = "entityId"
) = DepictedItem(name, description, imageUrl, instanceOfs, isSelected, id)

View file

@ -6,6 +6,7 @@ import android.net.ConnectivityManager;
import android.net.NetworkInfo; import android.net.NetworkInfo;
import android.telephony.TelephonyManager; import android.telephony.TelephonyManager;
import org.jetbrains.annotations.NotNull;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -28,34 +29,30 @@ public class NetworkUtilsTest {
@Test @Test
public void testInternetConnectionEstablished() { public void testInternetConnectionEstablished() {
Context mockContext = mock(Context.class); Context mockContext = getContext(true);
Application mockApplication = mock(Application.class);
ConnectivityManager mockConnectivityManager = mock(ConnectivityManager.class);
NetworkInfo mockNetworkInfo = mock(NetworkInfo.class);
when(mockNetworkInfo.isConnectedOrConnecting())
.thenReturn(true);
when(mockConnectivityManager.getActiveNetworkInfo())
.thenReturn(mockNetworkInfo);
when(mockApplication.getSystemService(Context.CONNECTIVITY_SERVICE))
.thenReturn(mockConnectivityManager);
when(mockContext.getApplicationContext()).thenReturn(mockApplication);
boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext); boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext);
assertTrue(internetConnectionEstablished); assertTrue(internetConnectionEstablished);
} }
@Test @NotNull
public void testInternetConnectionNotEstablished() { public static Context getContext(boolean connectionEstablished) {
Context mockContext = mock(Context.class); Context mockContext = mock(Context.class);
Application mockApplication = mock(Application.class); Application mockApplication = mock(Application.class);
ConnectivityManager mockConnectivityManager = mock(ConnectivityManager.class); ConnectivityManager mockConnectivityManager = mock(ConnectivityManager.class);
NetworkInfo mockNetworkInfo = mock(NetworkInfo.class); NetworkInfo mockNetworkInfo = mock(NetworkInfo.class);
when(mockNetworkInfo.isConnectedOrConnecting()) when(mockNetworkInfo.isConnectedOrConnecting())
.thenReturn(false); .thenReturn(connectionEstablished);
when(mockConnectivityManager.getActiveNetworkInfo()) when(mockConnectivityManager.getActiveNetworkInfo())
.thenReturn(mockNetworkInfo); .thenReturn(mockNetworkInfo);
when(mockApplication.getSystemService(Context.CONNECTIVITY_SERVICE)) when(mockApplication.getSystemService(Context.CONNECTIVITY_SERVICE))
.thenReturn(mockConnectivityManager); .thenReturn(mockConnectivityManager);
when(mockContext.getApplicationContext()).thenReturn(mockApplication); when(mockContext.getApplicationContext()).thenReturn(mockApplication);
return mockContext;
}
@Test
public void testInternetConnectionNotEstablished() {
Context mockContext = getContext(false);
boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext); boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext);
assertFalse(internetConnectionEstablished); assertFalse(internetConnectionEstablished);
} }

View file

@ -0,0 +1,74 @@
package fr.free.nrw.commons.utils
import android.database.Cursor
import androidx.lifecycle.LiveData
import androidx.paging.DataSource
import androidx.paging.LivePagedListBuilder
import androidx.paging.PagedList
import androidx.room.InvalidationTracker
import androidx.room.RoomDatabase
import androidx.room.RoomSQLiteQuery
import androidx.room.paging.LimitOffsetDataSource
import com.nhaarman.mockitokotlin2.whenever
import org.mockito.Mockito.mock
fun <T> List<T>.asPagedList(config: PagedList.Config? = null): LiveData<PagedList<T>> {
val defaultConfig = PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setPageSize(size)
.setMaxSize(size + 2)
.setPrefetchDistance(1)
.build()
return LivePagedListBuilder<Int, T>(
createMockDataSourceFactory(this),
config ?: defaultConfig
).build()
}
/**
* Provides a mocked instance of the data source factory
*/
fun <T> createMockDataSourceFactory(itemList: List<T>): DataSource.Factory<Int, T> =
object : DataSource.Factory<Int, T>() {
override fun create(): DataSource<Int, T> = MockLimitDataSource(itemList)
}
/**
* Provides a mocked Room SQL query
*/
private fun mockQuery(): RoomSQLiteQuery? {
val query = mock(RoomSQLiteQuery::class.java);
whenever(query.sql).thenReturn("");
return query;
}
/**
* Provides a mocked Room DB
*/
private fun mockDb(): RoomDatabase? {
val roomDatabase = mock(RoomDatabase::class.java);
val invalidationTracker = mock(InvalidationTracker::class.java)
whenever(roomDatabase.invalidationTracker).thenReturn(invalidationTracker);
return roomDatabase;
}
/**
* Class that defines the mocked data source
*/
class MockLimitDataSource<T>(private val itemList: List<T>) :
LimitOffsetDataSource<T>(mockDb(), mockQuery(), false, null) {
override fun convertRows(cursor: Cursor?): MutableList<T> = itemList.toMutableList()
override fun countItems(): Int = itemList.count()
override fun isInvalid(): Boolean = false
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<T>) {
}
override fun loadRange(startPosition: Int, loadCount: Int): MutableList<T> {
return itemList.subList(startPosition, startPosition + loadCount).toMutableList()
}
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<T>) {
callback.onResult(itemList, 0)
}
}