mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
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:
commit
350b19e62f
91 changed files with 2281 additions and 1185 deletions
14
README.md
14
README.md
|
|
@ -13,7 +13,7 @@ Initially started by the Wikimedia Foundation, this app is now maintained by gra
|
|||
|
||||
## 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]
|
||||
* [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/
|
||||
[3]: https://github.com/commons-app/apps-android-commons/issues
|
||||
|
||||
[4]: https://github.com/commons-app/apps-android-commons/wiki
|
||||
[5]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
|
||||
[6]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
|
||||
[7]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
|
||||
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation
|
||||
[9]: https://github.com/commons-app/apps-android-commons/wiki/Libraries-used
|
||||
[4]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-android-documentation
|
||||
[5]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-user-documentation
|
||||
[6]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#️-contributor-documentation
|
||||
[7]: https://github.com/commons-app/commons-app-documentation/blob/master/android/Volunteers-welcome!.md#volunteers-welcome
|
||||
[8]: https://github.com/commons-app/commons-app-documentation/blob/master/android/README.md#-developer-documentation
|
||||
[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
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
|
||||
implementation "com.hannesdorfmann:adapterdelegates4-pagination:$ADAPTER_DELEGATES_VERSION"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.os.Parcelable;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.media.Depictions;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
|
|
@ -15,6 +16,8 @@ import java.util.ArrayList;
|
|||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryPage;
|
||||
import org.wikipedia.gallery.ExtMetadata;
|
||||
|
|
@ -50,6 +53,8 @@ public class Media implements Parcelable {
|
|||
/**
|
||||
* Wikibase Identifier associated with media files
|
||||
*/
|
||||
@PrimaryKey
|
||||
@NonNull
|
||||
private String pageId;
|
||||
private List<String> categories; // as loaded at runtime?
|
||||
/**
|
||||
|
|
@ -64,14 +69,28 @@ public class Media implements Parcelable {
|
|||
* Provides local constructor
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param filename Media filename
|
||||
*/
|
||||
public Media(String filename) {
|
||||
public Media(final String filename) {
|
||||
this();
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
|
|
@ -86,11 +105,13 @@ public class Media implements Parcelable {
|
|||
* @param dateUploaded Media date uploaded
|
||||
* @param creator Media creator
|
||||
*/
|
||||
public Media(Uri localUri, String imageUrl, String filename,
|
||||
String description,
|
||||
long dataLength, Date dateCreated, Date dateUploaded, String creator) {
|
||||
public Media(final Uri localUri, final String imageUrl, final String filename,
|
||||
final String description,
|
||||
final long dataLength, final Date dateCreated, final Date dateUploaded,
|
||||
final String creator) {
|
||||
this();
|
||||
this.localUri = localUri;
|
||||
this.thumbUrl = imageUrl;
|
||||
thumbUrl = imageUrl;
|
||||
this.imageUrl = imageUrl;
|
||||
this.filename = filename;
|
||||
this.description = description;
|
||||
|
|
@ -100,17 +121,80 @@ public class Media implements Parcelable {
|
|||
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,
|
||||
description, -1, null, new Date(), creator);
|
||||
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);
|
||||
}
|
||||
|
||||
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.
|
||||
* 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
|
||||
*/
|
||||
@NonNull
|
||||
public static Media from(MwQueryPage page) {
|
||||
ImageInfo imageInfo = page.imageInfo();
|
||||
public static Media from(final MwQueryPage page) {
|
||||
final ImageInfo imageInfo = page.imageInfo();
|
||||
if (imageInfo == null) {
|
||||
return new Media(); // null is not allowed
|
||||
}
|
||||
ExtMetadata metadata = imageInfo.getMetadata();
|
||||
final ExtMetadata metadata = imageInfo.getMetadata();
|
||||
if (metadata == null) {
|
||||
Media media = new Media(null, imageInfo.getOriginalUrl(),
|
||||
final Media media = new Media(null, imageInfo.getOriginalUrl(),
|
||||
page.title(), "", 0, null, null, null);
|
||||
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
|
||||
media.setThumbUrl(imageInfo.getThumbUrl());
|
||||
|
|
@ -135,7 +219,7 @@ public class Media implements Parcelable {
|
|||
return media;
|
||||
}
|
||||
|
||||
Media media = new Media(null,
|
||||
final Media media = new Media(null,
|
||||
imageInfo.getOriginalUrl(),
|
||||
page.title(),
|
||||
"",
|
||||
|
|
@ -158,11 +242,12 @@ public class Media implements Parcelable {
|
|||
|
||||
media.setDescription(metadata.imageDescription());
|
||||
media.setCategories(MediaDataExtractorUtil.extractCategoriesFromList(metadata.getCategories()));
|
||||
String latitude = metadata.getGpsLatitude();
|
||||
String longitude = metadata.getGpsLongitude();
|
||||
final String latitude = metadata.getGpsLatitude();
|
||||
final String longitude = metadata.getGpsLongitude();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -175,29 +260,17 @@ public class Media implements Parcelable {
|
|||
* @param metadata
|
||||
* @return
|
||||
*/
|
||||
private static String getArtist(ExtMetadata metadata) {
|
||||
private static String getArtist(final ExtMetadata metadata) {
|
||||
try {
|
||||
String artistHtml = metadata.artist();
|
||||
final String artistHtml = metadata.artist();
|
||||
return artistHtml.substring(artistHtml.indexOf("title=\""), artistHtml.indexOf("\">"))
|
||||
.replace("title=\"User:", "");
|
||||
} catch (Exception ex) {
|
||||
} catch (final Exception ex) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getThumbUrl() {
|
||||
return thumbUrl;
|
||||
}
|
||||
|
|
@ -210,11 +283,13 @@ public class Media implements Parcelable {
|
|||
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Caption(if available) as the thumbnail title of the image
|
||||
*/
|
||||
public void setThumbnailTitle(String title) {
|
||||
this.thumbnailTitle = title;
|
||||
@Nullable
|
||||
private static Date safeParseDate(final String dateStr) {
|
||||
try {
|
||||
return CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr);
|
||||
} catch (final ParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -260,19 +335,17 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the name of the file.
|
||||
* @param filename the new name of the file
|
||||
*/
|
||||
public void setFilename(String filename) {
|
||||
this.filename = filename;
|
||||
* @return pageId for the current media object*/
|
||||
@NonNull
|
||||
public String getPageId() {
|
||||
return pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the discussion of the file.
|
||||
* @param discussion
|
||||
*sets pageId for the current media object
|
||||
*/
|
||||
public void setDiscussion(String discussion) {
|
||||
this.discussion = discussion;
|
||||
public void setPageId(final String pageId) {
|
||||
this.pageId = pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -310,11 +383,11 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the file description.
|
||||
* @param description the new description of the file
|
||||
* Sets the name of the file.
|
||||
* @param filename the new name of the file
|
||||
*/
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
public void setFilename(final String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -326,11 +399,11 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the dataLength of the file.
|
||||
* @param dataLength as a long
|
||||
* Sets the discussion of the file.
|
||||
* @param discussion
|
||||
*/
|
||||
public void setDataLength(long dataLength) {
|
||||
this.dataLength = dataLength;
|
||||
public void setDiscussion(final String discussion) {
|
||||
this.discussion = discussion;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -342,11 +415,11 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the creation date of the file.
|
||||
* @param date creation date as a Date
|
||||
* Sets the file description.
|
||||
* @param description the new description of the file
|
||||
*/
|
||||
public void setDateCreated(Date date) {
|
||||
this.dateCreated = date;
|
||||
public void setDescription(final String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -368,11 +441,11 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the creator name of the file.
|
||||
* @param creator creator name as a string
|
||||
* Sets the dataLength of the file.
|
||||
* @param dataLength as a long
|
||||
*/
|
||||
public void setCreator(String creator) {
|
||||
this.creator = creator;
|
||||
public void setDataLength(final long dataLength) {
|
||||
this.dataLength = dataLength;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -383,8 +456,11 @@ public class Media implements Parcelable {
|
|||
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() {
|
||||
|
|
@ -392,16 +468,11 @@ public class Media implements Parcelable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Sets the license name of the file.
|
||||
* @param license license name as a String
|
||||
* Sets the creator name of the file.
|
||||
* @param creator creator name as a string
|
||||
*/
|
||||
public void setLicenseInformation(String license, String licenseUrl) {
|
||||
this.license = license;
|
||||
|
||||
if (!licenseUrl.startsWith("http://") && !licenseUrl.startsWith("https://")) {
|
||||
licenseUrl = "https://" + licenseUrl;
|
||||
}
|
||||
this.licenseUrl = licenseUrl;
|
||||
public void setCreator(final String creator) {
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -413,12 +484,8 @@ public class Media implements Parcelable {
|
|||
return coordinates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the coordinates of where the file was created.
|
||||
* @param coordinates file coordinates as a LatLng
|
||||
*/
|
||||
public void setCoordinates(@Nullable LatLng coordinates) {
|
||||
this.coordinates = coordinates;
|
||||
public void setThumbUrl(final String thumbUrl) {
|
||||
this.thumbUrl = thumbUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -430,6 +497,27 @@ public class Media implements Parcelable {
|
|||
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.
|
||||
* </p>
|
||||
|
|
@ -437,26 +525,10 @@ public class Media implements Parcelable {
|
|||
* and then add the specified ones.
|
||||
* @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;
|
||||
}
|
||||
|
||||
@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
|
||||
* @return boolean requestedDeletion
|
||||
|
|
@ -465,12 +537,20 @@ public class Media implements Parcelable {
|
|||
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.
|
||||
*
|
||||
* @param license license name as a String
|
||||
*/
|
||||
public void setLicense(String license) {
|
||||
public void setLicense(final String license) {
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
|
|
@ -482,15 +562,10 @@ public class Media implements Parcelable {
|
|||
* This function sets captions
|
||||
* @param caption
|
||||
*/
|
||||
public void setCaption(String caption) {
|
||||
public void setCaption(final String 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) {
|
||||
this.localUri = localUri;
|
||||
}
|
||||
|
|
@ -516,6 +591,19 @@ public class Media implements Parcelable {
|
|||
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
|
||||
* activities.
|
||||
|
|
@ -523,63 +611,70 @@ public class Media implements Parcelable {
|
|||
* @param flags Parcel flag
|
||||
*/
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(this.localUri, flags);
|
||||
dest.writeString(this.thumbUrl);
|
||||
dest.writeString(this.imageUrl);
|
||||
dest.writeString(this.filename);
|
||||
dest.writeString(this.thumbnailTitle);
|
||||
dest.writeString(this.caption);
|
||||
dest.writeString(this.description);
|
||||
dest.writeString(this.discussion);
|
||||
dest.writeLong(this.dataLength);
|
||||
dest.writeLong(this.dateCreated != null ? this.dateCreated.getTime() : -1);
|
||||
dest.writeLong(this.dateUploaded != null ? this.dateUploaded.getTime() : -1);
|
||||
dest.writeString(this.license);
|
||||
dest.writeString(this.licenseUrl);
|
||||
dest.writeString(this.creator);
|
||||
dest.writeString(this.pageId);
|
||||
dest.writeStringList(this.categories);
|
||||
dest.writeParcelable(this.depictions, flags);
|
||||
dest.writeByte(this.requestedDeletion ? (byte) 1 : (byte) 0);
|
||||
dest.writeParcelable(this.coordinates, flags);
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeParcelable(localUri, flags);
|
||||
dest.writeString(thumbUrl);
|
||||
dest.writeString(imageUrl);
|
||||
dest.writeString(filename);
|
||||
dest.writeString(thumbnailTitle);
|
||||
dest.writeString(caption);
|
||||
dest.writeString(description);
|
||||
dest.writeString(discussion);
|
||||
dest.writeLong(dataLength);
|
||||
dest.writeLong(dateCreated != null ? dateCreated.getTime() : -1);
|
||||
dest.writeLong(dateUploaded != null ? dateUploaded.getTime() : -1);
|
||||
dest.writeString(license);
|
||||
dest.writeString(licenseUrl);
|
||||
dest.writeString(creator);
|
||||
dest.writeString(pageId);
|
||||
dest.writeStringList(categories);
|
||||
dest.writeParcelable(depictions, flags);
|
||||
dest.writeByte(requestedDeletion ? (byte) 1 : (byte) 0);
|
||||
dest.writeParcelable(coordinates, flags);
|
||||
}
|
||||
|
||||
protected Media(Parcel in) {
|
||||
this.localUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
this.thumbUrl = in.readString();
|
||||
this.imageUrl = in.readString();
|
||||
this.filename = in.readString();
|
||||
this.thumbnailTitle = in.readString();
|
||||
this.caption = in.readString();
|
||||
this.description = in.readString();
|
||||
this.discussion = in.readString();
|
||||
this.dataLength = in.readLong();
|
||||
long tmpDateCreated = in.readLong();
|
||||
this.dateCreated = tmpDateCreated == -1 ? null : new Date(tmpDateCreated);
|
||||
long tmpDateUploaded = in.readLong();
|
||||
this.dateUploaded = tmpDateUploaded == -1 ? null : new Date(tmpDateUploaded);
|
||||
this.license = in.readString();
|
||||
this.licenseUrl = in.readString();
|
||||
this.creator = in.readString();
|
||||
this.pageId = in.readString();
|
||||
final ArrayList<String> list = new ArrayList<>();
|
||||
in.readStringList(list);
|
||||
this.categories=list;
|
||||
in.readParcelable(Depictions.class.getClassLoader());
|
||||
this.requestedDeletion = in.readByte() != 0;
|
||||
this.coordinates = in.readParcelable(LatLng.class.getClassLoader());
|
||||
/**
|
||||
* Equals implementation that matches all parameters for equality check
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof Media)) {
|
||||
return false;
|
||||
}
|
||||
final Media media = (Media) o;
|
||||
return getDataLength() == media.getDataLength() &&
|
||||
isRequestedDeletion() == media.isRequestedDeletion() &&
|
||||
Objects.equals(getLocalUri(), media.getLocalUri()) &&
|
||||
Objects.equals(getThumbUrl(), media.getThumbUrl()) &&
|
||||
Objects.equals(getImageUrl(), media.getImageUrl()) &&
|
||||
Objects.equals(getFilename(), media.getFilename()) &&
|
||||
Objects.equals(getThumbnailTitle(), media.getThumbnailTitle()) &&
|
||||
Objects.equals(getCaption(), media.getCaption()) &&
|
||||
Objects.equals(getDescription(), media.getDescription()) &&
|
||||
Objects.equals(getDiscussion(), media.getDiscussion()) &&
|
||||
Objects.equals(getDateCreated(), media.getDateCreated()) &&
|
||||
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
|
||||
public Media createFromParcel(Parcel source) {
|
||||
return new Media(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Media[] newArray(int size) {
|
||||
return new Media[size];
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Hashcode implementation that uses all parameters for calculating hash
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects
|
||||
.hash(getLocalUri(), getThumbUrl(), getImageUrl(), getFilename(), getThumbnailTitle(),
|
||||
getCaption(), getDescription(), getDiscussion(), getDataLength(), getDateCreated(),
|
||||
getDateUploaded(), getLicense(), getLicenseUrl(), getCreator(), getPageId(),
|
||||
getCategories(), getDepictions(), isRequestedDeletion(), getCoordinates());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,6 @@
|
|||
package fr.free.nrw.commons;
|
||||
|
||||
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.IOException;
|
||||
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(),
|
||||
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() {
|
||||
return CLIENT;
|
||||
|
|
@ -45,7 +41,7 @@ public final class OkHttpConnectionFactory {
|
|||
|
||||
private static HttpLoggingInterceptor getLoggingInterceptor() {
|
||||
final HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor()
|
||||
.setLevel(Level.BASIC);
|
||||
.setLevel(Level.BASIC);
|
||||
|
||||
httpLoggingInterceptor.redactHeader("Authorization");
|
||||
httpLoggingInterceptor.redactHeader("Cookie");
|
||||
|
|
@ -54,7 +50,10 @@ public final class OkHttpConnectionFactory {
|
|||
}
|
||||
|
||||
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()
|
||||
.header("User-Agent", CommonsApplication.getInstance().getUserAgent())
|
||||
.build();
|
||||
|
|
@ -66,16 +65,18 @@ public final class OkHttpConnectionFactory {
|
|||
|
||||
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());
|
||||
if (rsp.isSuccessful()) {
|
||||
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()) {
|
||||
throw new IOException(body.string());
|
||||
}
|
||||
}
|
||||
}catch (final IOException e){
|
||||
} catch (final IOException e) {
|
||||
Timber.e(e);
|
||||
}
|
||||
return rsp;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import android.os.RemoteException;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import fr.free.nrw.commons.nearby.NearbyController;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
|
@ -74,8 +75,10 @@ public class BookmarkLocationsDao {
|
|||
boolean bookmarkExists = findBookmarkLocation(bookmarkLocation);
|
||||
if (bookmarkExists) {
|
||||
deleteBookmarkLocation(bookmarkLocation);
|
||||
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, false);
|
||||
} else {
|
||||
addBookmarkLocation(bookmarkLocation);
|
||||
NearbyController.updateMarkerLabelListBookmark(bookmarkLocation, true);
|
||||
}
|
||||
return !bookmarkExists;
|
||||
}
|
||||
|
|
@ -160,10 +163,9 @@ public class BookmarkLocationsDao {
|
|||
location,
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CATEGORY)),
|
||||
builder.build(),
|
||||
null,
|
||||
null
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_PIC)),
|
||||
cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESTROYED))
|
||||
);
|
||||
// TODO: add pic and destroyed to bookmark location dao
|
||||
}
|
||||
|
||||
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_LONG, bookmarkLocation.location.getLongitude());
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_PIC, bookmarkLocation.pic);
|
||||
cv.put(BookmarkLocationsDao.Table.COLUMN_DESTROYED, bookmarkLocation.destroyed);
|
||||
return cv;
|
||||
}
|
||||
|
||||
|
|
@ -197,6 +200,7 @@ public class BookmarkLocationsDao {
|
|||
static final String COLUMN_WIKIDATA_LINK = "location_wikidata_link";
|
||||
static final String COLUMN_COMMONS_LINK = "location_commons_link";
|
||||
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.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
|
|
@ -211,7 +215,8 @@ public class BookmarkLocationsDao {
|
|||
COLUMN_WIKIPEDIA_LINK,
|
||||
COLUMN_WIKIDATA_LINK,
|
||||
COLUMN_COMMONS_LINK,
|
||||
COLUMN_PIC
|
||||
COLUMN_PIC,
|
||||
COLUMN_DESTROYED
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
|
@ -228,7 +233,8 @@ public class BookmarkLocationsDao {
|
|||
+ COLUMN_WIKIPEDIA_LINK + " STRING,"
|
||||
+ COLUMN_WIKIDATA_LINK + " STRING,"
|
||||
+ COLUMN_COMMONS_LINK + " STRING,"
|
||||
+ COLUMN_PIC + " STRING"
|
||||
+ COLUMN_PIC + " STRING,"
|
||||
+ COLUMN_DESTROYED + " STRING"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
|
|
@ -242,37 +248,24 @@ public class BookmarkLocationsDao {
|
|||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
Timber.d("bookmarksLocations db is updated from:"+from+", to:"+to);
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
if (from < 7) {
|
||||
// doesn't exist yet
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 7) {
|
||||
// table added in version 8
|
||||
onCreate(db);
|
||||
from++;
|
||||
onUpdate(db, from, to);
|
||||
return;
|
||||
}
|
||||
if (from == 8) {
|
||||
from++;
|
||||
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;
|
||||
switch (from) {
|
||||
case 7: onCreate(db);
|
||||
case 8: // No change
|
||||
case 9: // No change
|
||||
case 10:
|
||||
try {
|
||||
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_pic STRING;");
|
||||
} catch (SQLiteException exception){
|
||||
Timber.e(exception);
|
||||
}
|
||||
case 11: // No change
|
||||
case 12:
|
||||
try {
|
||||
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;");
|
||||
}catch (SQLiteException exception){
|
||||
Timber.e(exception);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@ package fr.free.nrw.commons.category
|
|||
|
||||
import android.text.TextUtils
|
||||
import fr.free.nrw.commons.upload.GpsCategoryModel
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
|
||||
import fr.free.nrw.commons.utils.StringSortingUtils
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Function3
|
||||
import io.reactivex.functions.Function4
|
||||
import timber.log.Timber
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
|
@ -67,30 +68,42 @@ class CategoriesModel @Inject constructor(
|
|||
* @param imageTitleList
|
||||
* @return
|
||||
*/
|
||||
fun searchAll(term: String, imageTitleList: List<String>): Observable<List<CategoryItem>> {
|
||||
return suggestionsOrSearch(term, imageTitleList)
|
||||
fun searchAll(
|
||||
term: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>
|
||||
): Observable<List<CategoryItem>> {
|
||||
return suggestionsOrSearch(term, imageTitleList, selectedDepictions)
|
||||
.map { it.map { CategoryItem(it, false) } }
|
||||
}
|
||||
|
||||
private fun suggestionsOrSearch(term: String, imageTitleList: List<String>):
|
||||
Observable<List<String>> {
|
||||
private fun suggestionsOrSearch(
|
||||
term: String,
|
||||
imageTitleList: List<String>,
|
||||
selectedDepictions: List<DepictedItem>
|
||||
): Observable<List<String>> {
|
||||
return if (TextUtils.isEmpty(term))
|
||||
Observable.combineLatest(
|
||||
categoriesFromDepiction(selectedDepictions),
|
||||
gpsCategoryModel.categoriesFromLocation,
|
||||
titleCategories(imageTitleList),
|
||||
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
|
||||
Function3(::combine)
|
||||
Function4(::combine)
|
||||
)
|
||||
else
|
||||
categoryClient.searchCategoriesForPrefix(term.toLowerCase(), SEARCH_CATS_LIMIT)
|
||||
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
|
||||
}
|
||||
|
||||
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>) =
|
||||
Observable.just(selectedDepictions.map { it.commonsCategories }.flatten())
|
||||
|
||||
private fun combine(
|
||||
depictionCategories: List<String>,
|
||||
locationCategories: List<String>,
|
||||
titles: List<String>,
|
||||
recents: List<String>
|
||||
) = locationCategories + titles + recents
|
||||
) = depictionCategories + locationCategories + titles + recents
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -98,14 +111,13 @@ class CategoriesModel @Inject constructor(
|
|||
* @param titleList
|
||||
* @return
|
||||
*/
|
||||
private fun titleCategories(titleList: List<String>): Observable<List<String>> {
|
||||
return if (titleList.isNotEmpty())
|
||||
private fun titleCategories(titleList: List<String>) =
|
||||
if (titleList.isNotEmpty())
|
||||
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
|
||||
searchResults.map { it as List<String> }.flatten()
|
||||
}
|
||||
else
|
||||
Observable.just(emptyList())
|
||||
}
|
||||
|
||||
/**
|
||||
* Return category for single title
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions;
|
|||
|
||||
import android.os.Parcel;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
|
|
@ -13,7 +12,7 @@ import java.util.ArrayList;
|
|||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryLogEvent;
|
||||
import java.util.Objects;
|
||||
|
||||
@Entity(tableName = "contribution")
|
||||
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_IN_PROGRESS = 3;
|
||||
|
||||
@PrimaryKey (autoGenerate = true)
|
||||
private long _id;
|
||||
private int state;
|
||||
private long transferred;
|
||||
private String decimalCoords;
|
||||
private String dateCreatedSource;
|
||||
private WikidataPlace wikidataPlace;
|
||||
/**
|
||||
* Each depiction loaded in depictions activity is associated with a wikidata entity id,
|
||||
* this Id is in turn used to upload depictions to wikibase
|
||||
* Each depiction loaded in depictions activity is associated with a wikidata entity id, this Id
|
||||
* is in turn used to upload depictions to wikibase
|
||||
*/
|
||||
private List<DepictedItem> depictedItems = new ArrayList<>();
|
||||
private String mimeType;
|
||||
/**
|
||||
* This hasmap stores the list of multilingual captions, where
|
||||
* key of the HashMap is the language and value is the caption in the corresponding language
|
||||
* Ex: key = "en", value: "<caption in short in English>"
|
||||
* key = "de" , value: "<caption in german>"
|
||||
* This hasmap stores the list of multilingual captions, where key of the HashMap is the language
|
||||
* and value is the caption in the corresponding language Ex: key = "en", value: "<caption in
|
||||
* short in English>" key = "de" , value: "<caption in german>"
|
||||
*/
|
||||
private Map<String, String> captions = new HashMap<>();
|
||||
|
||||
|
|
@ -55,20 +51,13 @@ public class Contribution extends Media {
|
|||
UploadMediaDetail.formatList(item.getUploadMediaDetails()),
|
||||
sessionManager.getAuthorName(),
|
||||
categories);
|
||||
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
|
||||
captions = UploadMediaDetail.formatCaptions(item.getUploadMediaDetails());
|
||||
decimalCoords = item.getGpsCoords().getDecimalCoords();
|
||||
dateCreatedSource = "";
|
||||
this.depictedItems = depictedItems;
|
||||
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) {
|
||||
this.dateCreatedSource = dateCreatedSource;
|
||||
}
|
||||
|
|
@ -108,14 +97,6 @@ public class Contribution extends Media {
|
|||
return wikidataPlace;
|
||||
}
|
||||
|
||||
public long get_id() {
|
||||
return _id;
|
||||
}
|
||||
|
||||
public void set_id(final long _id) {
|
||||
this._id = _id;
|
||||
}
|
||||
|
||||
public String getDecimalCoords() {
|
||||
return decimalCoords;
|
||||
}
|
||||
|
|
@ -128,29 +109,30 @@ public class Contribution extends Media {
|
|||
this.depictedItems = depictedItems;
|
||||
}
|
||||
|
||||
public void setMimeType(String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
public void setMimeType(final String mimeType) {
|
||||
this.mimeType = mimeType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
|
||||
* This is a replacement of the previously used titles for images (titles were not multilingual)
|
||||
* Also now captions replace the previous convention of using title for filename
|
||||
*
|
||||
* Captions are a feature part of Structured data. They are meant to store short, multilingual
|
||||
* descriptions about files This is a replacement of the previously used titles for images (titles
|
||||
* 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
|
||||
*
|
||||
* <p>
|
||||
* returns list of captions stored in hashmap
|
||||
*/
|
||||
public Map<String, String> getCaptions() {
|
||||
return captions;
|
||||
return captions;
|
||||
}
|
||||
|
||||
public void setCaptions(Map<String, String> captions) {
|
||||
this.captions = captions;
|
||||
this.captions = captions;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -161,7 +143,6 @@ public class Contribution extends Media {
|
|||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
super.writeToParcel(dest, flags);
|
||||
dest.writeLong(_id);
|
||||
dest.writeInt(state);
|
||||
dest.writeLong(transferred);
|
||||
dest.writeString(decimalCoords);
|
||||
|
|
@ -169,9 +150,24 @@ public class Contribution extends Media {
|
|||
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) {
|
||||
super(in);
|
||||
_id = in.readLong();
|
||||
state = in.readInt();
|
||||
transferred = in.readLong();
|
||||
decimalCoords = in.readString();
|
||||
|
|
@ -190,4 +186,35 @@ public class Contribution extends Media {
|
|||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
|
|
@ -15,40 +15,55 @@ import java.util.List;
|
|||
@Dao
|
||||
public abstract class ContributionDao {
|
||||
|
||||
@Query("SELECT * FROM contribution order by dateUploaded DESC")
|
||||
abstract LiveData<List<Contribution>> fetchContributions();
|
||||
@Query("SELECT * FROM contribution order by dateUploaded DESC")
|
||||
abstract DataSource.Factory<Integer, Contribution> fetchContributions();
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
public abstract Single<Long> save(Contribution contribution);
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
public abstract void saveSynchronous(Contribution contribution);
|
||||
|
||||
public Completable deleteAllAndSave(List<Contribution> contributions){
|
||||
return Completable.fromAction(() -> deleteAllAndSaveTransaction(contributions));
|
||||
}
|
||||
public Completable save(final Contribution contribution) {
|
||||
return Completable
|
||||
.fromAction(() -> saveSynchronous(contribution));
|
||||
}
|
||||
|
||||
@Transaction
|
||||
public void deleteAllAndSaveTransaction(List<Contribution> contributions){
|
||||
deleteAll(Contribution.STATE_COMPLETED);
|
||||
save(contributions);
|
||||
}
|
||||
@Transaction
|
||||
public void deleteAndSaveContribution(final Contribution oldContribution,
|
||||
final Contribution newContribution) {
|
||||
deleteSynchronous(oldContribution);
|
||||
saveSynchronous(newContribution);
|
||||
}
|
||||
|
||||
@Insert
|
||||
public abstract void save(List<Contribution> contribution);
|
||||
public Completable saveAndDelete(final Contribution oldContribution,
|
||||
final Contribution newContribution) {
|
||||
return Completable
|
||||
.fromAction(() -> deleteAndSaveContribution(oldContribution, newContribution));
|
||||
}
|
||||
|
||||
@Delete
|
||||
public abstract Single<Integer> delete(Contribution contribution);
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
public abstract Single<List<Long>> save(List<Contribution> contribution);
|
||||
|
||||
@Query("SELECT * from contribution WHERE filename=:fileName")
|
||||
public abstract List<Contribution> getContributionWithTitle(String fileName);
|
||||
@Delete
|
||||
public abstract void deleteSynchronous(Contribution contribution);
|
||||
|
||||
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
|
||||
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
|
||||
public Completable delete(final Contribution contribution) {
|
||||
return Completable
|
||||
.fromAction(() -> deleteSynchronous(contribution));
|
||||
}
|
||||
|
||||
@Query("Delete FROM contribution")
|
||||
public abstract void deleteAll();
|
||||
@Query("SELECT * from contribution WHERE filename=:fileName")
|
||||
public abstract List<Contribution> getContributionWithTitle(String fileName);
|
||||
|
||||
@Query("Delete FROM contribution WHERE state = :state")
|
||||
public abstract void deleteAll(int state);
|
||||
@Query("UPDATE contribution SET state=:state WHERE state in (:toUpdateStates)")
|
||||
public abstract Single<Integer> updateStates(int state, int[] toUpdateStates);
|
||||
|
||||
@Update
|
||||
public abstract Single<Integer> update(Contribution contribution);
|
||||
@Query("Delete FROM contribution")
|
||||
public abstract void deleteAll();
|
||||
|
||||
@Update
|
||||
public abstract void updateSynchronous(Contribution contribution);
|
||||
|
||||
public Completable update(final Contribution contribution) {
|
||||
return Completable
|
||||
.fromAction(() -> updateSynchronous(contribution));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import fr.free.nrw.commons.media.MediaClient;
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.Random;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
||||
|
|
@ -39,23 +38,22 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
private int position;
|
||||
private Contribution contribution;
|
||||
private Random random = new Random();
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private final MediaClient mediaClient;
|
||||
|
||||
ContributionViewHolder(View parent, Callback callback,
|
||||
MediaClient mediaClient) {
|
||||
ContributionViewHolder(final View parent, final Callback callback,
|
||||
final MediaClient mediaClient) {
|
||||
super(parent);
|
||||
this.mediaClient = mediaClient;
|
||||
ButterKnife.bind(this, parent);
|
||||
this.callback=callback;
|
||||
}
|
||||
|
||||
public void init(int position, Contribution contribution) {
|
||||
public void init(final int position, final Contribution contribution) {
|
||||
this.contribution = contribution;
|
||||
fetchAndDisplayCaption(contribution);
|
||||
this.position = position;
|
||||
String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
|
||||
final String imageSource = chooseImageSource(contribution.getThumbUrl(), contribution.getLocalUri());
|
||||
if (!TextUtils.isEmpty(imageSource)) {
|
||||
final ImageRequest imageRequest =
|
||||
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
|
||||
|
|
@ -84,8 +82,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
stateView.setVisibility(View.GONE);
|
||||
progressView.setVisibility(View.VISIBLE);
|
||||
failedImageOptions.setVisibility(View.GONE);
|
||||
long total = contribution.getDataLength();
|
||||
long transferred = contribution.getTransferred();
|
||||
final long total = contribution.getDataLength();
|
||||
final long transferred = contribution.getTransferred();
|
||||
if (transferred == 0 || transferred >= total) {
|
||||
progressView.setIndeterminate(true);
|
||||
} else {
|
||||
|
|
@ -107,14 +105,14 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
*
|
||||
* @param contribution
|
||||
*/
|
||||
private void fetchAndDisplayCaption(Contribution contribution) {
|
||||
private void fetchAndDisplayCaption(final Contribution contribution) {
|
||||
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
|
||||
titleView.setText(contribution.getDisplayTitle());
|
||||
} else {
|
||||
final String pageId = contribution.getPageId();
|
||||
if (pageId != null) {
|
||||
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
|
||||
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
|
@ -141,7 +139,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
private String chooseImageSource(String thumbUrl, Uri localUri) {
|
||||
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
|
||||
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
|
||||
localUri != null ? localUri.toString() :
|
||||
null;
|
||||
|
|
|
|||
|
|
@ -12,16 +12,6 @@ public class ContributionsContract {
|
|||
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -31,8 +21,6 @@ public class ContributionsContract {
|
|||
|
||||
void deleteUpload(Contribution contribution);
|
||||
|
||||
Media getItemAtPosition(int i);
|
||||
|
||||
void updateContribution(Contribution contribution);
|
||||
|
||||
void fetchMediaDetails(Contribution contribution);
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import android.widget.Toast;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentManager.OnBackStackChangedListener;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
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.CampaignsPresenter;
|
||||
import fr.free.nrw.commons.campaigns.ICampaignsView;
|
||||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
||||
import fr.free.nrw.commons.contributions.ContributionsListFragment.SourceRefresher;
|
||||
import fr.free.nrw.commons.contributions.ContributionsListFragment.Callback;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
|
|
@ -54,7 +52,6 @@ import io.reactivex.Observable;
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
|
@ -62,11 +59,10 @@ import timber.log.Timber;
|
|||
public class ContributionsFragment
|
||||
extends CommonsDaggerSupportFragment
|
||||
implements
|
||||
MediaDetailProvider,
|
||||
OnBackStackChangedListener,
|
||||
SourceRefresher,
|
||||
LocationUpdateListener,
|
||||
ICampaignsView, ContributionsContract.View {
|
||||
MediaDetailProvider,
|
||||
ICampaignsView, ContributionsContract.View, Callback {
|
||||
@Inject @Named("default_preferences") JsonKvStore store;
|
||||
@Inject NearbyController nearbyController;
|
||||
@Inject OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
|
@ -78,8 +74,8 @@ public class ContributionsFragment
|
|||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private ContributionsListFragment contributionsListFragment;
|
||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
||||
private static final String CONTRIBUTION_LIST_FRAGMENT_TAG = "ContributionListFragmentTag";
|
||||
private MediaDetailPagerFragment mediaDetailPagerFragment;
|
||||
static final String MEDIA_DETAIL_PAGER_FRAGMENT_TAG = "MediaDetailFragmentTag";
|
||||
|
||||
@BindView(R.id.card_view_nearby) public NearbyNotificationCardView nearbyNotificationCardView;
|
||||
|
|
@ -113,7 +109,6 @@ public class ContributionsFragment
|
|||
}
|
||||
};
|
||||
private boolean shouldShowMediaDetailsFragment;
|
||||
private int numberOfContributions;
|
||||
private boolean isAuthCookieAcquired;
|
||||
|
||||
@Override
|
||||
|
|
@ -128,7 +123,6 @@ public class ContributionsFragment
|
|||
ButterKnife.bind(this, view);
|
||||
presenter.onAttachView(this);
|
||||
contributionsPresenter.onAttachView(this);
|
||||
contributionsPresenter.setLifeCycleOwner(this.getViewLifecycleOwner());
|
||||
campaignView.setVisibility(View.GONE);
|
||||
checkBoxView = View.inflate(getActivity(), R.layout.nearby_permission_dialog, null);
|
||||
checkBox = (CheckBox) checkBoxView.findViewById(R.id.never_ask_again);
|
||||
|
|
@ -141,103 +135,21 @@ public class ContributionsFragment
|
|||
|
||||
if (savedInstanceState != null) {
|
||||
mediaDetailPagerFragment = (MediaDetailPagerFragment) getChildFragmentManager()
|
||||
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
|
||||
.findFragmentByTag(MEDIA_DETAIL_PAGER_FRAGMENT_TAG);
|
||||
contributionsListFragment = (ContributionsListFragment) getChildFragmentManager()
|
||||
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
|
||||
.findFragmentByTag(CONTRIBUTION_LIST_FRAGMENT_TAG);
|
||||
shouldShowMediaDetailsFragment = savedInstanceState.getBoolean("mediaDetailsVisible");
|
||||
}
|
||||
|
||||
initFragments();
|
||||
|
||||
if(shouldShowMediaDetailsFragment){
|
||||
showMediaDetailPagerFragment();
|
||||
}else{
|
||||
showContributionsListFragment();
|
||||
}
|
||||
|
||||
if (!ConfigUtils.isBetaFlavour()) {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
|
@ -265,7 +177,7 @@ public class ContributionsFragment
|
|||
if (nearbyNotificationCardView != null) {
|
||||
if (store.getBoolean("displayNearbyCardView", true)) {
|
||||
if (nearbyNotificationCardView.cardViewVisibilityState
|
||||
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
== NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
nearbyNotificationCardView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
|
|
@ -275,20 +187,22 @@ public class ContributionsFragment
|
|||
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() {
|
||||
// hide tabs on media detail view is visible
|
||||
((MainActivity)getActivity()).hideTabs();
|
||||
((MainActivity) getActivity()).hideTabs();
|
||||
// hide nearby card view on media detail is visible
|
||||
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
|
||||
public void onBackStackChanged() {
|
||||
((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(){
|
||||
Intent intent = new Intent(getActivity(), UploadService.class);
|
||||
intent.setAction(UploadService.ACTION_START_SERVICE);
|
||||
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")
|
||||
private void setUploadCount() {
|
||||
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getUploadCount(((MainActivity)getActivity()).sessionManager.getCurrentAccount().name)
|
||||
.subscribeOn(Schedulers.io())
|
||||
|
|
@ -373,8 +286,6 @@ public class ContributionsFragment
|
|||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
boolean mediaDetailsVisible = mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible();
|
||||
outState.putBoolean("mediaDetailsVisible", mediaDetailsVisible);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -384,13 +295,6 @@ public class ContributionsFragment
|
|||
firstLocationUpdate = true;
|
||||
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)) {
|
||||
checkPermissionsAndShowNearbyCardView();
|
||||
if (nearbyNotificationCardView.cardViewVisibilityState == NearbyNotificationCardView.CardViewVisibilityState.READY) {
|
||||
|
|
@ -403,10 +307,6 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
fetchCampaigns();
|
||||
if(isAuthCookieAcquired){
|
||||
contributionsPresenter.fetchContributions();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void checkPermissionsAndShowNearbyCardView() {
|
||||
|
|
@ -463,17 +363,11 @@ public class ContributionsFragment
|
|||
}
|
||||
|
||||
private void updateNearbyNotification(@Nullable NearbyController.NearbyPlacesInfo nearbyPlacesInfo) {
|
||||
|
||||
if (nearbyPlacesInfo != null && nearbyPlacesInfo.placeList != null && nearbyPlacesInfo.placeList.size() > 0) {
|
||||
Place closestNearbyPlace = nearbyPlacesInfo.placeList.get(0);
|
||||
String distance = formatDistanceBetween(curLatLng, closestNearbyPlace.location);
|
||||
closestNearbyPlace.setDistance(distance);
|
||||
nearbyNotificationCardView.updateContent(closestNearbyPlace);
|
||||
if (mediaDetailPagerFragment != null && mediaDetailPagerFragment.isVisible()) {
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
}else {
|
||||
nearbyNotificationCardView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
// Means that no close nearby place is found
|
||||
nearbyNotificationCardView.setVisibility(View.GONE);
|
||||
|
|
@ -553,37 +447,13 @@ public class ContributionsFragment
|
|||
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
|
||||
*
|
||||
* @param contribution contribution to be retried
|
||||
*/
|
||||
private void retryUpload(Contribution contribution) {
|
||||
@Override
|
||||
public void retryUpload(Contribution contribution) {
|
||||
if (NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
if (contribution.getState() == STATE_FAILED && null != uploadService) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.ViewGroup;
|
||||
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.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 List<Contribution> contributions;
|
||||
|
||||
public ContributionsListAdapter(Callback callback,
|
||||
MediaClient mediaClient) {
|
||||
ContributionsListAdapter(final Callback callback,
|
||||
final MediaClient mediaClient) {
|
||||
super(DIFF_CALLBACK);
|
||||
this.callback = callback;
|
||||
this.mediaClient = mediaClient;
|
||||
contributions = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the new View Holder which will be used to display items(contributions)
|
||||
* using the onBindViewHolder(viewHolder,position)
|
||||
* Uses DiffUtil to calculate the changes in the list
|
||||
* It has methods that check ID and the content of the items to determine if its a new item
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public ContributionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
ContributionViewHolder viewHolder = new ContributionViewHolder(
|
||||
LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.layout_contribution, parent, false), callback, mediaClient);
|
||||
return viewHolder;
|
||||
}
|
||||
private static final DiffUtil.ItemCallback<Contribution> DIFF_CALLBACK =
|
||||
new DiffUtil.ItemCallback<Contribution>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(final Contribution oldContribution, final Contribution newContribution) {
|
||||
return oldContribution.getPageId().equals(newContribution.getPageId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ContributionViewHolder holder, int position) {
|
||||
final Contribution contribution = contributions.get(position);
|
||||
if (TextUtils.isEmpty(contribution.getThumbUrl())
|
||||
&& contribution.getState() == Contribution.STATE_COMPLETED) {
|
||||
callback.fetchMediaUriFor(contribution);
|
||||
}
|
||||
@Override
|
||||
public boolean areContentsTheSame(final Contribution oldContribution, final Contribution newContribution) {
|
||||
return oldContribution.equals(newContribution);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return contributions.size();
|
||||
}
|
||||
|
||||
public void setContributions(@NonNull List<Contribution> contributionList) {
|
||||
contributions = contributionList;
|
||||
notifyDataSetChanged();
|
||||
Contribution getContributionForPosition(final int position) {
|
||||
return getItem(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the new View Holder which will be used to display items(contributions) using the
|
||||
* onBindViewHolder(viewHolder,position)
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return contributions.get(position).get_id();
|
||||
public ContributionViewHolder onCreateViewHolder(@NonNull final ViewGroup parent,
|
||||
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 {
|
||||
|
|
@ -72,9 +75,5 @@ public class ContributionsListAdapter extends RecyclerView.Adapter<ContributionV
|
|||
void deleteUpload(Contribution contribution);
|
||||
|
||||
void openMediaDetail(int contribution);
|
||||
|
||||
Contribution getContributionForPosition(int position);
|
||||
|
||||
void fetchMediaUriFor(Contribution contribution);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import static android.view.View.VISIBLE;
|
|||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
|
@ -16,218 +17,217 @@ import android.widget.TextView;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
import fr.free.nrw.commons.Media;
|
||||
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.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
/**
|
||||
* 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";
|
||||
@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;
|
||||
private static final String RV_STATE = "rv_scroll_state";
|
||||
|
||||
@Inject @Named("default_preferences") JsonKvStore kvStore;
|
||||
@Inject ContributionController controller;
|
||||
@Inject MediaClient mediaClient;
|
||||
@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;
|
||||
|
||||
private Animation fab_close;
|
||||
private Animation fab_open;
|
||||
private Animation rotate_forward;
|
||||
private Animation rotate_backward;
|
||||
@Inject
|
||||
ContributionController controller;
|
||||
@Inject
|
||||
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 String lastVisibleItemID;
|
||||
private final Callback callback;
|
||||
|
||||
private int SPAN_COUNT=3;
|
||||
private List<Contribution> contributions=new ArrayList<>();
|
||||
private final int SPAN_COUNT_LANDSCAPE = 3;
|
||||
private final int SPAN_COUNT_PORTRAIT = 1;
|
||||
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
initAdapter();
|
||||
return view;
|
||||
ContributionsListFragment(final Callback callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
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() {
|
||||
adapter = new ContributionsListAdapter(callback, mediaClient);
|
||||
adapter.setHasStableIds(true);
|
||||
}
|
||||
@Override
|
||||
public void retryUpload(final Contribution contribution) {
|
||||
callback.retryUpload(contribution);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
initRecyclerView();
|
||||
initializeAnimations();
|
||||
setListeners();
|
||||
}
|
||||
@Override
|
||||
public void deleteUpload(final Contribution contribution) {
|
||||
contributionsListPresenter.deleteUpload(contribution);
|
||||
}
|
||||
|
||||
private void initRecyclerView() {
|
||||
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
rvContributionsList.setLayoutManager(new GridLayoutManager(getContext(),SPAN_COUNT));
|
||||
} else {
|
||||
rvContributionsList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
}
|
||||
@Override
|
||||
public void openMediaDetail(final int position) {
|
||||
callback.showDetail(position);
|
||||
}
|
||||
|
||||
rvContributionsList.setAdapter(adapter);
|
||||
adapter.setContributions(contributions);
|
||||
}
|
||||
public Media getMediaAtPosition(final int i) {
|
||||
return adapter.getContributionForPosition(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
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()));
|
||||
}
|
||||
}
|
||||
public int getTotalMediaCount() {
|
||||
return adapter.getItemCount();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
public interface Callback {
|
||||
|
||||
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(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 retryUpload(Contribution contribution);
|
||||
|
||||
void showDetail(int position);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,14 +1,13 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import androidx.paging.DataSource.Factory;
|
||||
import io.reactivex.Completable;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
|
|
@ -59,23 +58,23 @@ class ContributionsLocalDataSource {
|
|||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
public Single<Integer> deleteContribution(Contribution contribution) {
|
||||
public Completable deleteContribution(Contribution contribution) {
|
||||
return contributionDao.delete(contribution);
|
||||
}
|
||||
|
||||
public LiveData<List<Contribution>> getContributions() {
|
||||
public Factory<Integer, Contribution> getContributions() {
|
||||
return contributionDao.fetchContributions();
|
||||
}
|
||||
|
||||
public Completable saveContributions(List<Contribution> contributions) {
|
||||
return contributionDao.deleteAllAndSave(contributions);
|
||||
public Single<List<Long>> saveContributions(List<Contribution> contributions) {
|
||||
return contributionDao.save(contributions);
|
||||
}
|
||||
|
||||
public void set(String key, long value) {
|
||||
defaultKVStore.putLong(key,value);
|
||||
}
|
||||
|
||||
public Single<Integer> updateContribution(Contribution contribution) {
|
||||
public Completable updateContribution(Contribution contribution) {
|
||||
return contributionDao.update(contribution);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,8 @@ import fr.free.nrw.commons.CommonsApplication;
|
|||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.MediaDataExtractor;
|
||||
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.db.AppDatabase;
|
||||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.mwapi.UserClient;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
|
|
@ -25,6 +25,9 @@ import javax.inject.Inject;
|
|||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
/**
|
||||
* The presenter class for Contributions
|
||||
*/
|
||||
|
|
@ -35,25 +38,10 @@ public class ContributionsPresenter implements UserActionListener {
|
|||
private final Scheduler ioThreadScheduler;
|
||||
private CompositeDisposable compositeDisposable;
|
||||
private ContributionsContract.View view;
|
||||
private List<Contribution> contributionList=new ArrayList<>();
|
||||
|
||||
@Inject
|
||||
Context context;
|
||||
|
||||
@Inject
|
||||
UserClient userClient;
|
||||
|
||||
@Inject
|
||||
AppDatabase appDatabase;
|
||||
|
||||
@Inject
|
||||
SessionManager sessionManager;
|
||||
|
||||
@Inject
|
||||
MediaDataExtractor mediaDataExtractor;
|
||||
|
||||
private LifecycleOwner lifeCycleOwner;
|
||||
|
||||
@Inject
|
||||
ContributionsPresenter(ContributionsRepository repository, @Named(CommonsApplicationModule.MAIN_THREAD) Scheduler mainThreadScheduler,@Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) {
|
||||
this.repository = repository;
|
||||
|
|
@ -61,74 +49,12 @@ public class ContributionsPresenter implements UserActionListener {
|
|||
this.ioThreadScheduler=ioThreadScheduler;
|
||||
}
|
||||
|
||||
private String user;
|
||||
|
||||
@Override
|
||||
public void onAttachView(ContributionsContract.View view) {
|
||||
this.view = view;
|
||||
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
|
||||
public void onDetachView() {
|
||||
this.view = null;
|
||||
|
|
@ -146,24 +72,10 @@ public class ContributionsPresenter implements UserActionListener {
|
|||
*/
|
||||
@Override
|
||||
public void deleteUpload(Contribution contribution) {
|
||||
compositeDisposable.add(repository.deleteContributionFromDB(contribution)
|
||||
.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);
|
||||
compositeDisposable.add(repository
|
||||
.deleteContributionFromDB(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
package fr.free.nrw.commons.contributions;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import androidx.paging.DataSource.Factory;
|
||||
import io.reactivex.Completable;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
|
|
@ -33,7 +32,7 @@ public class ContributionsRepository {
|
|||
* @param contribution
|
||||
* @return
|
||||
*/
|
||||
public Single<Integer> deleteContributionFromDB(Contribution contribution) {
|
||||
public Completable deleteContributionFromDB(Contribution contribution) {
|
||||
return localDataSource.deleteContribution(contribution);
|
||||
}
|
||||
|
||||
|
|
@ -46,11 +45,11 @@ public class ContributionsRepository {
|
|||
return localDataSource.getContributionWithFileName(fileName);
|
||||
}
|
||||
|
||||
public LiveData<List<Contribution>> fetchContributions() {
|
||||
public Factory<Integer, Contribution> fetchContributions() {
|
||||
return localDataSource.getContributions();
|
||||
}
|
||||
|
||||
public Completable save(List<Contribution> contributions) {
|
||||
public Single<List<Long>> save(List<Contribution> contributions) {
|
||||
return localDataSource.saveContributions(contributions);
|
||||
}
|
||||
|
||||
|
|
@ -58,11 +57,7 @@ public class ContributionsRepository {
|
|||
localDataSource.set(key,value);
|
||||
}
|
||||
|
||||
public long getLong(String key) {
|
||||
return localDataSource.getLong(key);
|
||||
}
|
||||
|
||||
public Single<Integer> updateContribution(Contribution contribution) {
|
||||
public Completable updateContribution(Contribution contribution) {
|
||||
return localDataSource.updateContribution(contribution);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package fr.free.nrw.commons.contributions;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -12,7 +11,6 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.GravityCompat;
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
|
|
@ -20,16 +18,9 @@ import androidx.fragment.app.Fragment;
|
|||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentPagerAdapter;
|
||||
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.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.auth.SessionManager;
|
||||
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 io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.content.ContentResolver.requestSync;
|
||||
|
||||
public class MainActivity extends NavigationBaseActivity implements FragmentManager.OnBackStackChangedListener {
|
||||
|
||||
@BindView(R.id.tab_layout)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
|||
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
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";
|
||||
private final String DROP_TABLE_STATEMENT="DROP TABLE IF EXISTS %s";
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import fr.free.nrw.commons.contributions.ContributionDao
|
|||
* 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)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun contributionDao(): ContributionDao
|
||||
|
|
|
|||
|
|
@ -90,8 +90,9 @@ public interface DepictedImagesContract {
|
|||
|
||||
/**
|
||||
* 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)
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ public class DepictedImagesFragment extends DaggerFragment implements DepictedIm
|
|||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
handleNoInternet();
|
||||
} else {
|
||||
presenter.fetchMoreImages();
|
||||
presenter.fetchMoreImages(entityId);
|
||||
}
|
||||
}
|
||||
if (isLastPage) {
|
||||
|
|
|
|||
|
|
@ -80,10 +80,11 @@ public class DepictedImagesPresenter implements DepictedImagesContract.UserActio
|
|||
|
||||
/**
|
||||
* Fetches more images for the item and adds it to the grid view adapter
|
||||
* @param entityId
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
public void fetchMoreImages() {
|
||||
public void fetchMoreImages(String entityId) {
|
||||
view.progressBarVisible(true);
|
||||
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, queryList.size())
|
||||
.subscribeOn(ioScheduler)
|
||||
|
|
|
|||
|
|
@ -224,7 +224,9 @@ public class CommonsApplicationModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
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
|
||||
|
|
|
|||
|
|
@ -4,10 +4,11 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.paging.PagedListAdapter
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.upload.structure.depictions.DepictedItem
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
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 {
|
||||
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions), onDepictionClicked)
|
||||
return DepictedItemViewHolder(parent.inflate(R.layout.item_depictions))
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: DepictedItemViewHolder, position: Int) {
|
||||
holder.bind(getItem(position)!!)
|
||||
holder.bind(getItem(position)!!, onDepictionClicked)
|
||||
}
|
||||
}
|
||||
|
||||
class DepictedItemViewHolder(containerView: View, val onDepictionClicked: (DepictedItem) -> Unit) :
|
||||
BaseViewHolder<DepictedItem>(containerView) {
|
||||
|
||||
override fun bind(item: DepictedItem) {
|
||||
class DepictedItemViewHolder(override val containerView: View) :
|
||||
RecyclerView.ViewHolder(containerView), LayoutContainer {
|
||||
fun bind(item: DepictedItem, onDepictionClicked: (DepictedItem) -> Unit) {
|
||||
containerView.setOnClickListener { onDepictionClicked(item) }
|
||||
depicts_label.text = item.name
|
||||
description.text = item.description
|
||||
|
|
@ -44,7 +44,3 @@ class DepictedItemViewHolder(containerView: View, val onDepictionClicked: (Depic
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class DepictsClient @Inject constructor(
|
|||
}
|
||||
|
||||
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>> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ public class MediaClient {
|
|||
|
||||
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
|
||||
private Map<String, Map<String, String>> continuationStore;
|
||||
private Map<String, Boolean> continuationExists;
|
||||
public static final String NO_CAPTION = "No caption";
|
||||
private static final String NO_DEPICTION = "No depiction";
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ public class MediaClient {
|
|||
this.mediaInterface = mediaInterface;
|
||||
this.mediaDetailInterface = mediaDetailInterface;
|
||||
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
|
||||
* 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()) {
|
||||
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());
|
||||
})
|
||||
.map(Media::from)
|
||||
|
|
@ -169,7 +206,7 @@ public class MediaClient {
|
|||
* @return caption for image using wikibaseIdentifier
|
||||
*/
|
||||
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
|
||||
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
|
||||
return mediaDetailInterface.getEntityForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
|
||||
.map(mediaDetailResponse -> {
|
||||
if (isSuccess(mediaDetailResponse)) {
|
||||
for (Entity wikibaseItem : mediaDetailResponse.entities().values()) {
|
||||
|
|
@ -205,12 +242,16 @@ public class MediaClient {
|
|||
* @param entityId EntityId (Ex: Q81566) of the depict entity
|
||||
* @return label
|
||||
*/
|
||||
public Single<String> getLabelForDepiction(String entityId) {
|
||||
return getEntities(entityId)
|
||||
public Single<String> getLabelForDepiction(String entityId, String language) {
|
||||
return mediaDetailInterface.getEntity(entityId)
|
||||
.map(entities -> {
|
||||
if (isSuccess(entities)) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -220,6 +261,6 @@ public class MediaClient {
|
|||
}
|
||||
|
||||
public Single<Entities> getEntities(String entityId) {
|
||||
return mediaDetailInterface.getEntity(entityId, Locale.getDefault().getLanguage());
|
||||
return mediaDetailInterface.getEntity(entityId);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,7 +51,6 @@ import fr.free.nrw.commons.delete.ReasonBuilder;
|
|||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
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 io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
|
|||
|
|
@ -21,12 +21,11 @@ public interface MediaDetailInterface {
|
|||
|
||||
/**
|
||||
* 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")
|
||||
Single<Entities> getEntity(@Query("ids") String entityId, @Query("languages") String language);
|
||||
Single<Entities> getEntity(@Query("ids") String entityId);
|
||||
|
||||
/**
|
||||
* Fetches caption using wikibaseIdentifier
|
||||
|
|
@ -34,5 +33,5 @@ public interface MediaDetailInterface {
|
|||
* @param wikibaseIdentifier pageId for the media
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,26 +23,21 @@ import butterknife.ButterKnife;
|
|||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.Media;
|
||||
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.pictures.BookmarkPicturesContentProvider;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
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.ImageUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener {
|
||||
|
||||
@Inject SessionManager sessionManager;
|
||||
@Inject @Named("default_preferences") JsonKvStore store;
|
||||
@Inject BookmarkPicturesDao bookmarkDao;
|
||||
|
||||
@BindView(R.id.mediaDetailsPager) ViewPager pager;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public interface MediaInterface {
|
|||
String MEDIA_PARAMS="&prop=imageinfo&iiprop=url|extmetadata&iiurlwidth=640" +
|
||||
"&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal" +
|
||||
"|Artist|LicenseShortName|LicenseUrl";
|
||||
|
||||
/**
|
||||
* Checks if a page exists or not.
|
||||
*
|
||||
|
|
@ -47,6 +48,19 @@ public interface MediaInterface {
|
|||
MEDIA_PARAMS)
|
||||
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
|
||||
*
|
||||
|
|
@ -85,21 +99,15 @@ public interface MediaInterface {
|
|||
Observable<MwParseResponse> getPageHtml(@Query("page") String title);
|
||||
|
||||
/**
|
||||
* Fetches caption using file name
|
||||
*
|
||||
* @param filename name of the file to be used for fetching captions
|
||||
* */
|
||||
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1")
|
||||
Observable<MwQueryResponse> fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
|
||||
* Fetches list of images from a depiction entity
|
||||
*
|
||||
* @param query depictionEntityId
|
||||
* @param sroffset number od depictions already fetched, this is useful in implementing
|
||||
* pagination
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetches list of images from a depiction entity
|
||||
*
|
||||
* @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);
|
||||
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
|
||||
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query,
|
||||
@Query("sroffset") String sroffset);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@ import android.os.Parcel;
|
|||
|
||||
import androidx.annotation.DrawableRes;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
|
|
@ -17,6 +19,7 @@ import fr.free.nrw.commons.R;
|
|||
*/
|
||||
public enum Label {
|
||||
|
||||
BOOKMARKS("BOOKMARK", R.drawable.ic_filled_star_24dp),
|
||||
BUILDING("Q41176", R.drawable.round_icon_generic_building),
|
||||
HOUSE("Q3947", 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);
|
||||
return label == null ? UNKNOWN : label;
|
||||
}
|
||||
|
||||
public static List<Label> valuesAsList() {
|
||||
return Arrays.asList(Label.values());
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.content.Context;
|
|||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
|
||||
|
||||
import com.mapbox.mapboxsdk.annotations.IconFactory;
|
||||
|
|
@ -14,6 +15,7 @@ import java.util.ArrayList;
|
|||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -166,36 +168,6 @@ public class NearbyController {
|
|||
VectorDrawableCompat vectorDrawable = null;
|
||||
VectorDrawableCompat vectorDrawableGreen = 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;
|
||||
try {
|
||||
vectorDrawable = VectorDrawableCompat.create(
|
||||
|
|
@ -255,4 +227,19 @@ public class NearbyController {
|
|||
public LatLng curLatLng; // Current location when this places are populated
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import java.util.ArrayList;
|
|||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.nearby.presenter.NearbyParentFragmentPresenter;
|
||||
import java.util.Collections;
|
||||
|
||||
public class NearbyFilterSearchRecyclerViewAdapter
|
||||
extends RecyclerView.Adapter<NearbyFilterSearchRecyclerViewAdapter.RecyclerViewHolder>
|
||||
|
|
|
|||
|
|
@ -37,9 +37,9 @@ fun placeAdapterDelegate(
|
|||
}
|
||||
cameraButton.setOnClickListener { onCameraClicked(item) }
|
||||
galleryButton.setOnClickListener { onGalleryClicked(item) }
|
||||
bookmarkRowButton.setOnClickListener {
|
||||
bookmarkButtonImage.setOnClickListener {
|
||||
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)
|
||||
}
|
||||
iconOverflow.setOnClickListener { onOverflowIconClicked(item, it) }
|
||||
|
|
@ -59,7 +59,7 @@ fun placeAdapterDelegate(
|
|||
if (item.hasCommonsLink() || item.hasWikidataLink()) VISIBLE
|
||||
else GONE
|
||||
|
||||
bookmarkRowButtonImage.setImageResource(
|
||||
bookmarkButtonImage.setImageResource(
|
||||
if (bookmarkLocationDao.findBookmarkLocation(item))
|
||||
R.drawable.ic_round_star_filled_24px
|
||||
else
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import android.content.IntentFilter;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.VectorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
|
|
@ -33,6 +34,7 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.SearchView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
|
@ -427,7 +429,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
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() {
|
||||
@Override
|
||||
public void setCheckboxUnknown() {
|
||||
|
|
@ -1128,7 +1130,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
|
|||
// When label filter is engaged
|
||||
// then compare it against place's label
|
||||
if (selectedLabels != null && (selectedLabels.size() != 0 || !filterForPlaceState)
|
||||
&& !selectedLabels.contains(place.getLabel())) {
|
||||
&& (!selectedLabels.contains(place.getLabel())
|
||||
&& !(selectedLabels.contains(Label.BOOKMARKS) && markerPlaceGroup.getIsBookmarked()))) {
|
||||
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) {
|
||||
addPlaceToNearbyList(place);
|
||||
|
||||
final VectorDrawableCompat vectorDrawable;
|
||||
if (isBookmarked) {
|
||||
vectorDrawable = VectorDrawableCompat.create(
|
||||
getContext().getResources(), R.drawable.ic_custom_bookmark_marker, getContext().getTheme()
|
||||
);
|
||||
} 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()) {
|
||||
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
|
||||
getContext().getResources(), getIconFor(place, isBookmarked), getContext().getTheme());
|
||||
|
||||
for (Marker marker : mapBox.getMarkers()) {
|
||||
if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
|
||||
|
||||
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
|
||||
* but it is transparent more than grey(as the name of the icon might suggest)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package fr.free.nrw.commons.nearby.presenter;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import com.mapbox.mapboxsdk.annotations.Marker;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
|
@ -301,6 +303,7 @@ public class NearbyParentFragmentPresenter
|
|||
}
|
||||
|
||||
@Override
|
||||
@MainThread
|
||||
public void updateMapMarkersToController(List<NearbyBaseMarker> nearbyBaseMarkers) {
|
||||
NearbyController.markerExistsMap = new HashMap<>();
|
||||
NearbyController.markerNeedPicMap = new HashMap<>();
|
||||
|
|
@ -308,7 +311,7 @@ public class NearbyParentFragmentPresenter
|
|||
for (int i = 0; i < nearbyBaseMarkers.size(); i++) {
|
||||
NearbyBaseMarker nearbyBaseMarker = nearbyBaseMarkers.get(i);
|
||||
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
|
||||
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());
|
||||
|
|
|
|||
|
|
@ -108,10 +108,12 @@ public class UploadRepository {
|
|||
*
|
||||
* @param query
|
||||
* @param imageTitleList
|
||||
* @param selectedDepictions
|
||||
* @return
|
||||
*/
|
||||
public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList) {
|
||||
return categoriesModel.searchAll(query, imageTitleList);
|
||||
public Observable<List<CategoryItem>> searchAll(String query, List<String> imageTitleList,
|
||||
List<DepictedItem> selectedDepictions) {
|
||||
return categoriesModel.searchAll(query, imageTitleList, selectedDepictions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ public class Prefs {
|
|||
public static String TRACKING_ENABLED = "eventLogging";
|
||||
public static final String DEFAULT_LICENSE = "defaultLicense";
|
||||
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 KEY_LANGUAGE_VALUE = "languageDescription";
|
||||
public static final String KEY_THEME_VALUE = "appThemePref";
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
prepareLanguages();
|
||||
Preference betaTesterPreference = findPreference("becomeBetaTester");
|
||||
|
|
@ -121,7 +84,6 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
findPreference("displayNearbyCardView").setEnabled(false);
|
||||
findPreference("displayLocationPermissionForCardView").setEnabled(false);
|
||||
findPreference("displayCampaignsCardView").setEnabled(false);
|
||||
uploadLimit.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,9 +23,14 @@ import fr.free.nrw.commons.di.CommonsDaggerService;
|
|||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
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.schedulers.Schedulers;
|
||||
import java.io.File;
|
||||
|
|
@ -33,6 +38,7 @@ import java.io.IOException;
|
|||
import java.text.ParseException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -106,10 +112,10 @@ public class UploadService extends CommonsDaggerService {
|
|||
notificationManager.notify(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
||||
|
||||
contribution.setTransferred(transferred);
|
||||
compositeDisposable.add(contributionDao.
|
||||
save(contribution).subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe());
|
||||
|
||||
compositeDisposable.add(contributionDao.update(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -156,14 +162,11 @@ public class UploadService extends CommonsDaggerService {
|
|||
Timber.d("%d uploads left", toUpload);
|
||||
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_IN_PROGRESS, curNotification.build());
|
||||
}
|
||||
|
||||
compositeDisposable.add(contributionDao
|
||||
.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(aLong->{
|
||||
contribution.set_id(aLong);
|
||||
uploadContribution(contribution);
|
||||
}, Throwable::printStackTrace));
|
||||
.subscribe(() -> uploadContribution(contribution)));
|
||||
}
|
||||
|
||||
private boolean freshStart = true;
|
||||
|
|
@ -269,7 +272,7 @@ public class UploadService extends CommonsDaggerService {
|
|||
}
|
||||
|
||||
private void onUpload(Contribution contribution, String notificationTag,
|
||||
UploadResult uploadResult) throws ParseException {
|
||||
UploadResult uploadResult) {
|
||||
Timber.d("Stash upload response 2 is %s", uploadResult.toString());
|
||||
|
||||
notificationManager.cancel(notificationTag, NOTIFICATION_UPLOAD_IN_PROGRESS);
|
||||
|
|
@ -282,8 +285,7 @@ public class UploadService extends CommonsDaggerService {
|
|||
}
|
||||
}
|
||||
|
||||
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult)
|
||||
throws ParseException {
|
||||
private void onSuccessfulUpload(Contribution contribution, UploadResult uploadResult) {
|
||||
compositeDisposable
|
||||
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
|
||||
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
|
||||
|
|
@ -293,17 +295,11 @@ public class UploadService extends CommonsDaggerService {
|
|||
saveCompletedContribution(contribution, uploadResult);
|
||||
}
|
||||
|
||||
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) throws ParseException {
|
||||
contribution.setFilename(uploadResult.createCanonicalFileName());
|
||||
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
|
||||
contribution.setState(Contribution.STATE_COMPLETED);
|
||||
contribution.setDateUploaded(CommonsDateUtil.getIso8601DateFormatTimestamp()
|
||||
.parse(uploadResult.getImageinfo().getTimestamp()));
|
||||
compositeDisposable.add(contributionDao
|
||||
.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe());
|
||||
private void saveCompletedContribution(Contribution contribution, UploadResult uploadResult) {
|
||||
compositeDisposable.add(mediaClient.getMedia("File:" + uploadResult.getFilename())
|
||||
.map(media -> new Contribution(media, Contribution.STATE_COMPLETED))
|
||||
.flatMapCompletable(newContribution -> contributionDao.saveAndDelete(contribution, newContribution))
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
|
|
@ -317,10 +313,11 @@ public class UploadService extends CommonsDaggerService {
|
|||
notificationManager.notify(contribution.getLocalUri().toString(), NOTIFICATION_UPLOAD_FAILED, curNotification.build());
|
||||
|
||||
contribution.setState(Contribution.STATE_FAILED);
|
||||
compositeDisposable.add(contributionDao.save(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe());
|
||||
|
||||
compositeDisposable.add(contributionDao
|
||||
.update(contribution)
|
||||
.subscribeOn(ioThreadScheduler)
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
private String findUniqueFilename(String fileName) throws IOException {
|
||||
|
|
|
|||
|
|
@ -60,7 +60,7 @@ class CategoriesPresenter @Inject constructor(
|
|||
}
|
||||
|
||||
private fun searchResults(term: String) =
|
||||
repository.searchAll(term, getImageTitleList())
|
||||
repository.searchAll(term, getImageTitleList(), repository.selectedDepictions)
|
||||
.subscribeOn(ioScheduler)
|
||||
.map { it.filterNot { categoryItem -> repository.containsYear(categoryItem.name) } }
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
@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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -107,7 +107,6 @@ class DepictsPresenter @Inject constructor(
|
|||
view.noDepictionSelected()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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.upload.WikidataItem
|
||||
import fr.free.nrw.commons.wikidata.WikidataProperties
|
||||
import fr.free.nrw.commons.wikidata.WikidataProperties.*
|
||||
import org.wikipedia.wikidata.DataValue
|
||||
import org.wikipedia.wikidata.Entities
|
||||
import org.wikipedia.wikidata.Statement_partial
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Model class for Depicted Item in Upload and Explore
|
||||
|
|
@ -17,14 +19,15 @@ data class DepictedItem constructor(
|
|||
val description: String?,
|
||||
val imageUrl: String?,
|
||||
val instanceOfs: List<String>,
|
||||
val commonsCategories: List<String>,
|
||||
var isSelected: Boolean,
|
||||
override val id: String
|
||||
) : WikidataItem {
|
||||
|
||||
constructor(entity: Entities.Entity) : this(
|
||||
entity,
|
||||
entity.labels().values.firstOrNull()?.value() ?: "",
|
||||
entity.descriptions().values.firstOrNull()?.value() ?: ""
|
||||
entity.labels().byLanguageOrFirstOrEmpty(),
|
||||
entity.descriptions().byLanguageOrFirstOrEmpty()
|
||||
)
|
||||
|
||||
constructor(entity: Entities.Entity, place: Place) : this(
|
||||
|
|
@ -36,10 +39,12 @@ data class DepictedItem constructor(
|
|||
constructor(entity: Entities.Entity, name: String, description: String) : this(
|
||||
name,
|
||||
description,
|
||||
entity[WikidataProperties.IMAGE].primaryImageValue?.let {
|
||||
entity[IMAGE].primaryImageValue?.let {
|
||||
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,
|
||||
entity.id()
|
||||
)
|
||||
|
|
@ -57,7 +62,7 @@ data class DepictedItem constructor(
|
|||
}
|
||||
|
||||
private fun List<Statement_partial>?.toIds(): List<String> {
|
||||
return this?.map { it.mainSnak.dataValue }
|
||||
return this?.map { it.mainSnak.dataValue }
|
||||
?.filterIsInstance<DataValue.EntityId>()
|
||||
?.map { it.value.id }
|
||||
?: emptyList()
|
||||
|
|
@ -69,3 +74,5 @@ private val List<Statement_partial>?.primaryImageValue: DataValue.ValueString?
|
|||
operator fun Entities.Entity.get(property: WikidataProperties) =
|
||||
statements?.get(property.propertyName)
|
||||
|
||||
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
|
||||
let { it[Locale.getDefault().language] ?: it.values.firstOrNull() }?.value() ?: ""
|
||||
|
|
|
|||
|
|
@ -21,6 +21,16 @@ public class CommonsDateUtil {
|
|||
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
|
||||
* @return timestamp
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package fr.free.nrw.commons.wikidata
|
|||
import fr.free.nrw.commons.BuildConfig
|
||||
|
||||
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");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
29
app/src/main/res/layout/fragment_search_depictions.xml
Normal file
29
app/src/main/res/layout/fragment_search_depictions.xml
Normal 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>
|
||||
|
|
@ -6,13 +6,21 @@
|
|||
android:focusableInTouchMode="true"
|
||||
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
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="@dimen/dimen_40"
|
||||
android:layout_height="@dimen/dimen_40"
|
||||
android:layout_marginLeft="@dimen/standard_gap"
|
||||
android:layout_marginStart="@dimen/standard_gap"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:layout_alignStart="@id/bookmarkButtonImage"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/empty_photo"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,42 +1,14 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/buttonLayout"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
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 android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/buttonLayout"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:layout_marginTop="@dimen/standard_gap"
|
||||
android:layout_below="@+id/icon"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/cameraButton"
|
||||
|
|
@ -151,4 +123,4 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -103,29 +103,28 @@
|
|||
android:background="@color/divider_grey" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/depicts_next"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="@string/next" />
|
||||
android:id="@+id/depicts_next"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginRight="24dp"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:text="@string/next" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/depicts_previous"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_toStartOf="@id/depicts_next"
|
||||
android:layout_toLeftOf="@id/depicts_next"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:text="@string/previous" />
|
||||
android:id="@+id/depicts_previous"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="24dp"
|
||||
android:layout_marginEnd="@dimen/standard_gap"
|
||||
android:layout_marginRight="@dimen/standard_gap"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_toLeftOf="@id/depicts_next"
|
||||
android:layout_toStartOf="@id/depicts_next"
|
||||
android:text="@string/previous" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
|||
|
|
@ -70,8 +70,10 @@
|
|||
<string name="provider_contributions">بارگذاریهای من</string>
|
||||
<string name="menu_share">به اشتراکگذاشتن</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_caption_hint">شرح مختصر (محدود به ۲۵۵ نویسه)</string>
|
||||
<string name="login_failed_network">قادر به ورود نیست - شکست شبکهای</string>
|
||||
<string name="login_failed_wrong_credentials">ورود به سیستم امکان پذیر نیست . لطفا نام کاربری و رمز عبور خود را بررسی کنید.</string>
|
||||
<string name="login_failed_throttled">تلاش ناموفق بیش از حد. لطفاً چند دقیقهٔ دیگر دوباره تلاش کنید</string>
|
||||
|
|
@ -175,6 +177,7 @@
|
|||
<string name="detail_panel_cats_label">ردهها</string>
|
||||
<string name="detail_panel_cats_loading">در حال بارگیری…</string>
|
||||
<string name="detail_panel_cats_none">هیچکدام انتخابنشده</string>
|
||||
<string name="detail_caption_empty">فاقد شرح</string>
|
||||
<string name="detail_description_empty">بدون توضیحات</string>
|
||||
<string name="detail_discussion_empty">بدون بحث</string>
|
||||
<string name="detail_license_empty">مجوز ناشناخته</string>
|
||||
|
|
@ -193,6 +196,7 @@
|
|||
<string name="upload">بارگذاری</string>
|
||||
<string name="yes">بله</string>
|
||||
<string name="no">خیر</string>
|
||||
<string name="media_detail_caption">شرح مختصر</string>
|
||||
<string name="media_detail_title">عنوان</string>
|
||||
<string name="media_detail_description">توضیح</string>
|
||||
<string name="media_detail_discussion">بحث</string>
|
||||
|
|
@ -255,6 +259,7 @@
|
|||
<string name="error_while_cache">خطا در زمان دریافت تصاویر</string>
|
||||
<string name="title_info">عنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، میتوانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.</string>
|
||||
<string name="description_info">لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شدهاست؟ شامل چه چیزی میشود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفهنظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر میرسد آن را شرح دهید.</string>
|
||||
<string name="caption_info">لطفاً شرح مختصری برای این تصویر بنویسید. (محدود به ۲۵۵ نویسه)</string>
|
||||
<string name="upload_image_too_dark">این تصویر خیلی تار است. آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری است که ارزش دانشنامهای داشته باشند.</string>
|
||||
<string name="upload_image_blurry">این تصویر خیلی محو است. آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری است که ارزش دانشنامهای داشته باشند.</string>
|
||||
<string name="upload_problem_exist">مشکلات احتمالی با این تصویر :</string>
|
||||
|
|
@ -385,6 +390,7 @@
|
|||
<string name="list_sheet">فهرست</string>
|
||||
<string name="storage_permission">اجازه ذخیره</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="previous">قبلی</string>
|
||||
<string name="submit">ارسال</string>
|
||||
|
|
@ -473,6 +479,7 @@
|
|||
<string name="share_via">اشتراک از طریق...</string>
|
||||
<string name="image_info">اطلاعات عکس</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_failed">ناموفق</string>
|
||||
<string name="delete_helper_ask_spam_selfie">یک سلفی</string>
|
||||
|
|
|
|||
|
|
@ -618,4 +618,5 @@
|
|||
<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="use_location_from_similar_image">Avez-vous pris ces deux photos du même endroit ? Voulez-vous utiliser les latitude/longitude de l’image sur la droite ?</string>
|
||||
<string name="nearby_no_results">Pas d’endroit trouvé, essayez de modifier vos critères de recherche.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<string name="preference_category_privacy">Persónuvernd</string>
|
||||
<string name="preference_category_location">Staðsetning</string>
|
||||
<string name="app_name">Commons</string>
|
||||
<string name="bullet">• \</string>
|
||||
<string name="bullet">• \\</string>
|
||||
<string name="menu_settings">Stillingar</string>
|
||||
<string name="intent_share_upload_label">Senda inn á Commons</string>
|
||||
<string name="username">Notandanafn</string>
|
||||
|
|
@ -92,7 +92,7 @@
|
|||
<string name="title_activity_category_details">Flokkur</string>
|
||||
<string name="title_activity_review">Yfirlestur jafningja</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 <a href=\"%1$s\">GitHub tilkynningar (issue)</a> til að koma villum og uppástungum á framfæri.</string>
|
||||
<string name="about_privacy_policy">Meðferð persónuupplýsinga</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="quiz">Spurningaleikur</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="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>
|
||||
|
|
@ -361,7 +361,7 @@
|
|||
<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_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_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>
|
||||
|
|
|
|||
|
|
@ -620,4 +620,5 @@
|
|||
<string name="ask_to_turn_location_on">להפעיל מיקום?</string>
|
||||
<string name="nearby_needs_location">פעולת \"בסביבה\" זקוקה לשירותי מיקומי פועלים כדי לעבוד כמו שצריך</string>
|
||||
<string name="use_location_from_similar_image">האם צילמת את שתי התמונות באותו המקום? האם ברצונך להשתמש בקו הרוחב וקו האורך של התמונה משמאל?</string>
|
||||
<string name="nearby_no_results">לא נמצאו מקומות, נא לנסות לשנות את החיפוש.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Authors:
|
||||
* Abijeet Patro
|
||||
* Diki Ananta
|
||||
* N219
|
||||
* NoiX180
|
||||
* Notanotheramy
|
||||
-->
|
||||
|
|
@ -15,11 +17,11 @@
|
|||
<string name="bullet">•</string>
|
||||
<string name="menu_settings">Setèlan</string>
|
||||
<string name="intent_share_upload_label">Unggah menyang Commons</string>
|
||||
<string name="username">Jeneng panganggo</string>
|
||||
<string name="password">Tembung wadi</string>
|
||||
<string name="username">Jeneng naraguna</string>
|
||||
<string name="password">Tembung sandi</string>
|
||||
<string name="login_credential">Mlebu log akun Commons Beta panjenengan</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="logging_in_title">Lagi mlebu log</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_description_hint">Wedharan</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_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_generic">Wurung mlebu log</string>
|
||||
<string name="share_upload_button">Unggah</string>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
<!-- Authors:
|
||||
* Abijeet Patro
|
||||
* CYAN
|
||||
* Codenstory
|
||||
* Dlsrks1021
|
||||
* Doyoon1995
|
||||
* Freebiekr
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
<string name="provider_modifications">바뀜</string>
|
||||
<string name="menu_upload_single">올리기</string>
|
||||
<string name="categories_search_text_hint">분류 검색</string>
|
||||
<string name="depicts_search_text_hint">미디어가 서술한 항목을 검색하세요. (산, 타지마할 등)</string>
|
||||
<string name="menu_save_categories">저장</string>
|
||||
<string name="refresh_button">새로 고침</string>
|
||||
<string name="display_list_button">목록</string>
|
||||
|
|
@ -98,6 +100,9 @@
|
|||
</plurals>
|
||||
<string name="categories_not_found">%1$s와(과) 일치하는 분류를 찾을 수 없습니다</string>
|
||||
<string name="depictions_not_found">%1$s에 대한 위키데이터 검색 결과가 없습니다</string>
|
||||
<string name="no_child_classes">%1$s에 자식 클래스가 없습니다</string>
|
||||
<string name="no_parent_classes">%1$s에 부모 클래스가 없습니다</string>
|
||||
<string name="depictions_image_not_found">서술을 위한 이미지가 없습니다</string>
|
||||
<string name="categories_skip_explanation">위키미디어 공용에서 그림을 더 찾기 쉽게 만들기 위해 분류를 추가합니다.\n분류를 추가하려면 입력을 시작하세요.</string>
|
||||
<string name="categories_activity_title">분류</string>
|
||||
<string name="title_activity_settings">설정</string>
|
||||
|
|
@ -113,6 +118,7 @@
|
|||
<string name="menu_feedback">(이메일로) 피드백 보내기</string>
|
||||
<string name="no_email_client">설치된 이메일 클라이언트가 없습니다</string>
|
||||
<string name="provider_categories">최근에 사용한 분류</string>
|
||||
<string name="provider_depictions">최근에 사용한 서술</string>
|
||||
<string name="waiting_first_sync">첫 동기화를 기다리는 중…</string>
|
||||
<string name="no_uploads_yet">아직 사진을 올리지 않았습니다.</string>
|
||||
<string name="menu_retry_upload">다시 시도</string>
|
||||
|
|
@ -173,6 +179,7 @@
|
|||
<string name="detail_panel_cats_loading">불러오는 중…</string>
|
||||
<string name="detail_panel_cats_none">선택하지 않음</string>
|
||||
<string name="detail_caption_empty">설명 없음</string>
|
||||
<string name="detail_depiction_empty">서술 없음</string>
|
||||
<string name="detail_description_empty">설명 없음</string>
|
||||
<string name="detail_discussion_empty">토론 없음</string>
|
||||
<string name="detail_license_empty">알 수 없는 라이선스</string>
|
||||
|
|
@ -193,6 +200,7 @@
|
|||
<string name="no">아니오</string>
|
||||
<string name="media_detail_caption">설명</string>
|
||||
<string name="media_detail_title">제목</string>
|
||||
<string name="media_detail_depiction">서술</string>
|
||||
<string name="media_detail_description">설명</string>
|
||||
<string name="media_detail_discussion">토론</string>
|
||||
<string name="media_detail_author">저자</string>
|
||||
|
|
@ -253,6 +261,7 @@
|
|||
<string name="error_while_cache">그림 캐시 처리 오류</string>
|
||||
<string name="title_info">이 파일을 설명할 수 있는 제목으로, 파일 이름으로 사용됩니다. 띄어쓰기를 포함한 일반적인 단어를 사용할 수 있습니다. 파일 확장자는 포함하지 마세요</string>
|
||||
<string name="description_info">미디어에 대해 가능한 많이 설명하십시오: 어디서 촬영한 것인가? 무엇을 보여주는가? 무슨 문맥을 가지는가? 물건이나 사람에 대해 설명하십시오. 풍경에서 시간을 알려주는 것처럼 쉽게 추측할 수 없는 정보를 제공합니다. 미디어가 평범하지 않다면 무엇이 이를 평범하지 않게 만들었는지 설명하십시오.</string>
|
||||
<string name="caption_info">그림 설명을 작성해 주세요. (255자 제한)</string>
|
||||
<string name="upload_image_too_dark">이 사진은 너무 어둡습니다. 정말 업로드하시겠습니까? 위키미디어 공용은 백과사전적인 가치가 있는 사진을 위한 공간입니다.</string>
|
||||
<string name="upload_image_blurry">이 사진은 흐릿합니다. 정말 업로드하시겠습니까? 위키미디어 공용은 백과사전적인 가치가 있는 사진을 위한 공간입니다.</string>
|
||||
<string name="upload_problem_exist">이 그림에 잠재적인 문제가 있습니다:</string>
|
||||
|
|
@ -326,6 +335,7 @@
|
|||
<string name="search_recent_header">최근 검색:</string>
|
||||
<string name="provider_searches">최근 검색된 쿼리</string>
|
||||
<string name="error_loading_categories">분류를 불러오는 동안 오류가 발생했습니다.</string>
|
||||
<string name="error_loading_depictions">서술을 불러오는 동안 오류가 발생했습니다.</string>
|
||||
<string name="error_loading_subcategories">하위 분류를 불러오는 동안 오류가 발생했습니다.</string>
|
||||
<string name="search_tab_title_media">미디어</string>
|
||||
<string name="search_tab_title_categories">분류</string>
|
||||
|
|
@ -426,7 +436,8 @@
|
|||
<string name="desc_language_Asia">아시아</string>
|
||||
<string name="desc_language_Pacific">태평양</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="nearby_card_permission_title">권한 요청</string>
|
||||
<string name="nearby_card_permission_explanation">사진이 필요한 주변 장소를 표시하기 위해 현재 위치를 사용하시겠습니까?</string>
|
||||
|
|
@ -477,6 +488,7 @@
|
|||
<string name="share_via">앱 공유...</string>
|
||||
<string name="image_info">이미지 정보</string>
|
||||
<string name="no_categories_found">분류가 없습니다</string>
|
||||
<string name="no_depiction_found">서술이 발견되지 않았습니다</string>
|
||||
<string name="upload_cancelled">업로드 취소됨</string>
|
||||
<string name="default_description_language">기본 설명 언어</string>
|
||||
<string name="delete_helper_show_deletion_title_success">성공</string>
|
||||
|
|
@ -500,6 +512,8 @@
|
|||
<string name="place_type">장소 유형:</string>
|
||||
<string name="nearby_search_hint">다리, 박물관, 호텔 등.</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_bookmark">북마크</string>
|
||||
<string name="title_app_shortcut_setting">설정</string>
|
||||
|
|
@ -511,4 +525,5 @@
|
|||
<string name="theme_dark_name">어두움</string>
|
||||
<string name="theme_light_name">밝음</string>
|
||||
<string name="ask_to_turn_location_on">위치를 켭니까?</string>
|
||||
<string name="nearby_no_results">발견된 장소가 없습니다. 검색 기준을 바꾸어 보십시오.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -594,4 +594,5 @@
|
|||
<string name="ask_to_turn_location_on">Да ја вклучам местоположбата?</string>
|
||||
<string name="nearby_needs_location">„Во близина“ бара местоположба за да работи</string>
|
||||
<string name="use_location_from_similar_image">Дали ги направивте ови две слики на истото место? Дали сакате да ја искористите географската ширина/должина од десната слика?</string>
|
||||
<string name="nearby_no_results">Не пронајдов места. Изменете ги критериумите во барањето.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -592,4 +592,5 @@
|
|||
<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="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>
|
||||
|
|
|
|||
|
|
@ -606,4 +606,5 @@
|
|||
<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="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>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
* Redredsonia
|
||||
* Rubin16
|
||||
* Vlad5250
|
||||
* VoxelJ
|
||||
* Wikisaurus
|
||||
* Wirbel78
|
||||
* Евгения
|
||||
|
|
@ -76,6 +77,7 @@
|
|||
<string name="menu_share">Поделиться</string>
|
||||
<string name="menu_open_in_browser">Открыть в браузере</string>
|
||||
<string name="share_title_hint">Подпись (Обязательна)</string>
|
||||
<string name="add_caption_toast">Пожалуйста, укажите название этого файла</string>
|
||||
<string name="share_description_hint">Описание</string>
|
||||
<string name="login_failed_network">Не удаётся войти — сбой сети</string>
|
||||
<string name="login_failed_wrong_credentials">Не удаётся войти — проверьте ваше имя пользователя и пароль</string>
|
||||
|
|
@ -537,6 +539,7 @@
|
|||
<string name="images_featured_explanation">Избранные изображения обычно сделаны профессиональными фотографами и иллюстраторами. Такие изображения отмечены сообществом участников Викисклада как имеющие высшее качество на этом вебсайте.</string>
|
||||
<string name="images_via_nearby_explanation">Изображения, загруженные участниками, которые находили места с помощью функционала \"Места поблизости\".</string>
|
||||
<string name="thanks_received_explanation">Это функция позволяет участникам послать благодарность другим участникам за их полезные правки с помощью маленькой ссылки на странице истории страницы или странице разницы версий.</string>
|
||||
<string name="previous_image_caption_description">Скопировать предыдущие название и описание</string>
|
||||
<string name="previous_button_tooltip_message">Нажмите, чтобы использовать название и описание, которые вы ввели для предыдущего изображения, а потом потом модифицировать их</string>
|
||||
<string name="welcome_do_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="ask_to_turn_location_on">Включить местоположение?</string>
|
||||
<string name="nearby_needs_location">Рядом необходимо, чтобы местоположение было включено для правильной работы.</string>
|
||||
<string name="nearby_no_results">Мест не найдено, попытайтесь изменить критерии поиска.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -53,8 +53,10 @@
|
|||
<string name="provider_contributions">Moje obrázky</string>
|
||||
<string name="menu_share">Zdieľať</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_caption_hint">Popisok (max. 255 znakov)</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_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="menu_upload_single">Nahrať</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="refresh_button">Obnoviť</string>
|
||||
<string name="display_list_button">Zoznam</string>
|
||||
|
|
@ -87,6 +90,7 @@
|
|||
<item quantity="other">%1$d nahrania</item>
|
||||
</plurals>
|
||||
<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_activity_title">Kategórie</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_loading">Načítava sa…</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_discussion_empty">Žiadna diskusia</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="no_nearby">V okolí sa nenašli žiadne miesta</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">Nahrať</string>
|
||||
<string name="yes">Áno</string>
|
||||
|
|
@ -566,4 +572,5 @@
|
|||
<string name="theme_default_name" fuzzy="true">Predvolený</string>
|
||||
<string name="theme_dark_name">Tmavý</string>
|
||||
<string name="theme_light_name">Svetlý</string>
|
||||
<string name="ask_to_turn_location_on">Zapnúť lokáciu?</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -598,4 +598,5 @@
|
|||
<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="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>
|
||||
|
|
|
|||
|
|
@ -610,4 +610,5 @@
|
|||
<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="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>
|
||||
|
|
|
|||
|
|
@ -106,6 +106,9 @@
|
|||
</plurals>
|
||||
<string name="categories_not_found">Категорій, відповідних %1$s, не знайдено</string>
|
||||
<string name="depictions_not_found">Не знайдено елементів вікіданих, що відповідають %1$s</string>
|
||||
<string name="no_child_classes">%1$s не має дочірніх класів</string>
|
||||
<string name="no_parent_classes">%1$s не має батьківських класів</string>
|
||||
<string name="depictions_image_not_found">Немає зображення для описування</string>
|
||||
<string name="categories_skip_explanation">Додайте категорії, щоб Ваші зображення було легше знайти у Вікісховищі. \n\nПочніть вводити текст, щоб додати категорії.</string>
|
||||
<string name="categories_activity_title">Категорії</string>
|
||||
<string name="title_activity_settings">Налаштування</string>
|
||||
|
|
@ -206,6 +209,7 @@
|
|||
<string name="no">Ні</string>
|
||||
<string name="media_detail_caption">Підпис</string>
|
||||
<string name="media_detail_title">Назва</string>
|
||||
<string name="media_detail_depiction">Описи зображеного</string>
|
||||
<string name="media_detail_description">Опис</string>
|
||||
<string name="media_detail_discussion">Обговорення</string>
|
||||
<string name="media_detail_author">Автор</string>
|
||||
|
|
@ -347,9 +351,11 @@
|
|||
<string name="search_recent_header">Недавні пошуки:</string>
|
||||
<string name="provider_searches">Недавні пошукові запити</string>
|
||||
<string name="error_loading_categories">Сталася помилка під час завантаження категорій.</string>
|
||||
<string name="error_loading_depictions">Сталася помилка при завантаженні описів зображеного.</string>
|
||||
<string name="error_loading_subcategories">Сталася помилка під час завантаження підкатегорій.</string>
|
||||
<string name="search_tab_title_media">Медіафайли</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_mobile">Завантаження з мобільного</string>
|
||||
<string name="successful_wikidata_edit">Зображення додано до сторінки %1$s у Вікіданих!</string>
|
||||
|
|
@ -455,6 +461,7 @@
|
|||
<string name="desc_language_Pacific">Океанія</string>
|
||||
<string name="no_categories_selected">Жодної категорії не вибрано</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="upload_flow_all_images_in_set">(Для всіх зображень у наборі)</string>
|
||||
<string name="search_this_area">Шукати в цій зоні</string>
|
||||
|
|
@ -555,6 +562,7 @@
|
|||
<string name="share_via">Поділитися програмкою через…</string>
|
||||
<string name="image_info">Інформація про зображення</string>
|
||||
<string name="no_categories_found">Категорії не знайдені</string>
|
||||
<string name="no_depiction_found">Описів зображеного не знайдено</string>
|
||||
<string name="upload_cancelled">Завантаження скасовано</string>
|
||||
<string name="previous_image_title_description_not_found">Відсутній заголовок або опис попереднього зображення</string>
|
||||
<string name="dialog_box_text_nomination">Чому %1$s має бути видалено?</string>
|
||||
|
|
@ -587,6 +595,9 @@
|
|||
<string name="place_type">Тип місця:</string>
|
||||
<string name="nearby_search_hint">Міст, музей, готель тощо</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_description">Чи це — фото місця %1$s?</string>
|
||||
<string name="title_app_shortcut_explore">Дослідити</string>
|
||||
|
|
@ -605,4 +616,5 @@
|
|||
<string name="ask_to_turn_location_on">Увімкнути визначення місця розташування?</string>
|
||||
<string name="nearby_needs_location">«Поблизу» потребує увімкненого визначення місця розташування, щоб працювати належним чином</string>
|
||||
<string name="use_location_from_similar_image">Ви зробили ці два знімки в одному й тому ж місці? Хочете використати широту/довготу зображення справа?</string>
|
||||
<string name="nearby_no_results">Місць не знайдено, спробуйте змінити критерії пошуку.</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -603,4 +603,5 @@
|
|||
<string name="ask_to_turn_location_on">打開位置?</string>
|
||||
<string name="nearby_needs_location">附近功能需要啟用位置才能運作正常</string>
|
||||
<string name="use_location_from_similar_image">您是否在同一地點拍攝了這兩張圖片?您要使用圖片右側的緯度/經度嗎?</string>
|
||||
<string name="nearby_no_results">查無地點,請嘗試更改您的搜尋條件。</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
* ZhaoGang
|
||||
* 予弦
|
||||
* 佛壁灯
|
||||
* 列维劳德
|
||||
* 夢蝶葬花
|
||||
* 沈澄心
|
||||
* 神樂坂秀吉
|
||||
|
|
@ -74,6 +75,7 @@
|
|||
<string name="menu_share">分享</string>
|
||||
<string name="menu_open_in_browser">在浏览器中查看</string>
|
||||
<string name="share_title_hint" fuzzy="true">标题 (要求)</string>
|
||||
<string name="add_caption_toast">请提供此文件的描述</string>
|
||||
<string name="share_description_hint">描述</string>
|
||||
<string name="login_failed_network">无法登录 - 网络故障</string>
|
||||
<string name="login_failed_wrong_credentials">无法登录——请检查您的用户名和密码</string>
|
||||
|
|
|
|||
|
|
@ -26,13 +26,6 @@
|
|||
android:summary="@string/use_external_storage_summary"
|
||||
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
|
||||
android:key="descriptionDefaultLanguagePref"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
|
|
|
|||
23
app/src/test/kotlin/ModelFunctions.kt
Normal file
23
app/src/test/kotlin/ModelFunctions.kt
Normal 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)
|
||||
|
|
@ -35,7 +35,9 @@ class BookMarkLocationDaoTest {
|
|||
COLUMN_WIKIDATA_LINK,
|
||||
COLUMN_COMMONS_LINK,
|
||||
COLUMN_LAT,
|
||||
COLUMN_LONG)
|
||||
COLUMN_LONG,
|
||||
COLUMN_PIC,
|
||||
COLUMN_DESTROYED)
|
||||
private val client: ContentProviderClient = mock()
|
||||
private val database: SQLiteDatabase = mock()
|
||||
private val captor = argumentCaptor<ContentValues>()
|
||||
|
|
@ -93,6 +95,8 @@ class BookMarkLocationDaoTest {
|
|||
assertEquals(builder.build().wikipediaLink, it.siteLinks.wikipediaLink)
|
||||
assertEquals(builder.build().wikidataLink, it.siteLinks.wikidataLink)
|
||||
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))
|
||||
verify(client).insert(eq(BASE_URI), captor.capture())
|
||||
captor.firstValue.let { cv ->
|
||||
assertEquals(11, cv.size())
|
||||
assertEquals(12, cv.size())
|
||||
assertEquals(examplePlaceBookmark.name, cv.getAsString(COLUMN_NAME))
|
||||
assertEquals(examplePlaceBookmark.longDescription, cv.getAsString(COLUMN_DESCRIPTION))
|
||||
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.commonsLink.toString(), cv.getAsString(COLUMN_COMMONS_LINK))
|
||||
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)
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
for (i in 0 until rowCount) {
|
||||
addRow(listOf("placeName", "placeDescription","placeCategory", exampleLabel.text, exampleLabel.icon,
|
||||
exampleUri, builder.build().wikipediaLink, builder.build().wikidataLink, builder.build().commonsLink,
|
||||
exampleLocation.latitude, exampleLocation.longitude))
|
||||
exampleLocation.latitude, exampleLocation.longitude, "picName", "placeDestroyed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package fr.free.nrw.commons.category
|
||||
|
||||
import categoryItem
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import depictedItem
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
||||
import fr.free.nrw.commons.upload.GpsCategoryModel
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.subjects.BehaviorSubject
|
||||
|
|
@ -36,11 +39,11 @@ class CategoriesModelTest {
|
|||
|
||||
// Checking if both return "Test"
|
||||
val expectedItems = expectedList.map { CategoryItem(it, false) }
|
||||
categoriesModel.searchAll("tes", emptyList())
|
||||
categoriesModel.searchAll("tes", emptyList(), emptyList())
|
||||
.test()
|
||||
.assertValues(expectedItems)
|
||||
|
||||
categoriesModel.searchAll("Tes", emptyList())
|
||||
categoriesModel.searchAll("Tes", emptyList(), emptyList())
|
||||
.test()
|
||||
.assertValues(expectedItems)
|
||||
}
|
||||
|
|
@ -48,6 +51,7 @@ class CategoriesModelTest {
|
|||
@Test
|
||||
fun `searchAll with empty search terms creates results from gps, title search & recents`() {
|
||||
val gpsCategoryModel: GpsCategoryModel = mock()
|
||||
val depictedItem = depictedItem(commonsCategories = listOf("depictionCategory"))
|
||||
|
||||
whenever(gpsCategoryModel.categoriesFromLocation)
|
||||
.thenReturn(BehaviorSubject.createDefault(listOf("gpsCategory")))
|
||||
|
|
@ -55,13 +59,14 @@ class CategoriesModelTest {
|
|||
.thenReturn(Observable.just(listOf("titleSearch")))
|
||||
whenever(categoryDao.recentCategories(25)).thenReturn(listOf("recentCategories"))
|
||||
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
|
||||
.searchAll("", listOf("tes"))
|
||||
.searchAll("", listOf("tes"), listOf(depictedItem))
|
||||
.test()
|
||||
.assertValue(
|
||||
listOf(
|
||||
CategoryItem("gpsCategory", false),
|
||||
CategoryItem("titleSearch", false),
|
||||
CategoryItem("recentCategories", false)
|
||||
categoryItem("depictionCategory"),
|
||||
categoryItem("gpsCategory"),
|
||||
categoryItem("titleSearch"),
|
||||
categoryItem("recentCategories")
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -7,9 +7,11 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.loader.content.CursorLoader
|
||||
import androidx.loader.content.Loader
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Scheduler
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
|
|
@ -17,9 +19,11 @@ import org.junit.Before
|
|||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.ArgumentMatchers.*
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* The unit test class for ContributionsPresenter
|
||||
|
|
@ -42,7 +46,7 @@ class ContributionsPresenterTest {
|
|||
|
||||
@Rule @JvmField var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
lateinit var scheduler : Scheduler
|
||||
lateinit var scheduler : TestScheduler
|
||||
|
||||
/**
|
||||
* initial setup
|
||||
|
|
@ -60,23 +64,13 @@ class ContributionsPresenterTest {
|
|||
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
|
||||
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)
|
||||
verify(repository).deleteContributionFromDB(contribution)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil
|
||||
import io.reactivex.Observable
|
||||
|
|
@ -7,18 +8,16 @@ import junit.framework.Assert.*
|
|||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.mockito.*
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
import org.wikipedia.dataclient.mwapi.ImageDetails
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryPage
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResult
|
||||
import org.wikipedia.gallery.ImageInfo
|
||||
import org.mockito.ArgumentCaptor
|
||||
import org.mockito.ArgumentMatchers.*
|
||||
import java.util.*
|
||||
import org.mockito.Captor
|
||||
|
||||
|
||||
import org.mockito.Mockito.*
|
||||
|
||||
|
||||
class MediaClientTest {
|
||||
|
|
@ -46,9 +45,10 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`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)
|
||||
}
|
||||
|
||||
|
|
@ -63,9 +63,10 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`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)
|
||||
}
|
||||
|
||||
|
|
@ -80,7 +81,7 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
|
||||
assertTrue(checkFileExistsUsingSha)
|
||||
|
|
@ -97,7 +98,7 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`when`(mediaInterface!!.checkFileExistsUsingSha(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
val checkFileExistsUsingSha = mediaClient!!.checkFileExistsUsingSha("abcde").blockingGet()
|
||||
assertFalse(checkFileExistsUsingSha)
|
||||
|
|
@ -117,7 +118,7 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
assertEquals("Test", mediaClient!!.getMedia("abcde").blockingGet().filename)
|
||||
}
|
||||
|
|
@ -136,10 +137,11 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`when`(mediaInterface!!.getMedia(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
assertEquals(Media.EMPTY, mediaClient!!.getMedia("abcde").blockingGet())
|
||||
}
|
||||
|
||||
@Captor
|
||||
private val filenameCaptor: ArgumentCaptor<String>? = null
|
||||
|
||||
|
|
@ -159,18 +161,18 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
|
||||
`when`(mediaInterface!!.getMediaWithGenerator(filenameCaptor!!.capture()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
assertEquals("Test", mediaClient!!.getPictureOfTheDay().blockingGet().filename)
|
||||
assertEquals(template, filenameCaptor.value);
|
||||
}
|
||||
|
||||
|
||||
@Captor
|
||||
private val continuationCaptor: ArgumentCaptor<Map<String, String>>? = null
|
||||
|
||||
@Test
|
||||
fun getMediaListFromCategoryTwice() {
|
||||
val mockContinuation= mapOf(Pair("gcmcontinue", "test"))
|
||||
val mockContinuation = mapOf(Pair("gcmcontinue", "test"))
|
||||
val imageInfo = ImageInfo()
|
||||
|
||||
val mwQueryPage = mock(MwQueryPage::class.java)
|
||||
|
|
@ -184,9 +186,13 @@ class MediaClientTest {
|
|||
`when`(mockResponse.query()).thenReturn(mwQueryResult)
|
||||
`when`(mockResponse.continuation()).thenReturn(mockContinuation)
|
||||
|
||||
`when`(mediaInterface!!.getMediaListFromCategory(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(),
|
||||
continuationCaptor!!.capture()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
`when`(
|
||||
mediaInterface!!.getMediaListFromCategory(
|
||||
ArgumentMatchers.anyString(), ArgumentMatchers.anyInt(),
|
||||
continuationCaptor!!.capture()
|
||||
)
|
||||
)
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
val media1 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
|
||||
val media2 = mediaClient!!.getMediaListFromCategory("abcde").blockingGet().get(0)
|
||||
|
||||
|
|
@ -197,6 +203,38 @@ class MediaClientTest {
|
|||
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
|
||||
fun getPageHtmlTest() {
|
||||
val mwParseResult = mock(MwParseResult::class.java)
|
||||
|
|
@ -207,7 +245,7 @@ class MediaClientTest {
|
|||
mockResponse.setParse(mwParseResult)
|
||||
|
||||
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
assertEquals("Test", mediaClient!!.getPageHtml("abcde").blockingGet())
|
||||
}
|
||||
|
|
@ -218,7 +256,7 @@ class MediaClientTest {
|
|||
mockResponse.setParse(null)
|
||||
|
||||
`when`(mediaInterface!!.getPageHtml(ArgumentMatchers.anyString()))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
.thenReturn(Observable.just(mockResponse))
|
||||
|
||||
assertEquals("", mediaClient!!.getPageHtml("abcde").blockingGet())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
package fr.free.nrw.commons.upload
|
||||
|
||||
import categoryItem
|
||||
import com.nhaarman.mockitokotlin2.*
|
||||
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.upload.categories.CategoriesContract
|
||||
import fr.free.nrw.commons.upload.categories.CategoriesPresenter
|
||||
|
|
@ -27,11 +27,6 @@ class CategoriesPresenterTest {
|
|||
|
||||
private lateinit var testScheduler: TestScheduler
|
||||
|
||||
private val categoryItems: ArrayList<CategoryItem> = ArrayList()
|
||||
|
||||
@Mock
|
||||
lateinit var categoryItem: CategoryItem
|
||||
|
||||
/**
|
||||
* initial setup
|
||||
*/
|
||||
|
|
@ -40,7 +35,6 @@ class CategoriesPresenterTest {
|
|||
fun setUp() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
testScheduler = TestScheduler()
|
||||
categoryItems.add(categoryItem)
|
||||
categoriesPresenter = CategoriesPresenter(repository, testScheduler, testScheduler)
|
||||
categoriesPresenter.onAttachView(view)
|
||||
}
|
||||
|
|
@ -62,7 +56,7 @@ class CategoriesPresenterTest {
|
|||
emptyCaptionUploadItem
|
||||
)
|
||||
)
|
||||
whenever(repository.searchAll("test", listOf("nonEmpty")))
|
||||
whenever(repository.searchAll("test", listOf("nonEmpty"), repository.selectedDepictions))
|
||||
.thenReturn(
|
||||
Observable.just(
|
||||
listOf(
|
||||
|
|
@ -87,7 +81,7 @@ class CategoriesPresenterTest {
|
|||
@Test
|
||||
fun `searchForCategoriesTest sets Error when list is empty`() {
|
||||
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())
|
||||
categoriesPresenter.searchForCategories("test")
|
||||
testScheduler.triggerActions()
|
||||
|
|
@ -124,10 +118,8 @@ class CategoriesPresenterTest {
|
|||
*/
|
||||
@Test
|
||||
fun onCategoryItemClickedTest() {
|
||||
val categoryItem = categoryItem()
|
||||
categoriesPresenter.onCategoryItemClicked(categoryItem)
|
||||
verify(repository).onCategoryClicked(categoryItem)
|
||||
}
|
||||
|
||||
private fun categoryItem(name: String = "name", selected: Boolean = false) =
|
||||
CategoryItem(name, selected)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
|||
import com.jraska.livedata.test
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import depictedItem
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
||||
import fr.free.nrw.commons.repository.UploadRepository
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsContract
|
||||
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 io.reactivex.Flowable
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
|
|
@ -62,8 +62,8 @@ class DepictsPresenterTest {
|
|||
depictedItem(id="nonUnique"),
|
||||
depictedItem(id="nonUnique"),
|
||||
depictedItem(
|
||||
id = "unique",
|
||||
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id)
|
||||
instanceOfs = listOf(WikidataDisambiguationItems.CATEGORY.id),
|
||||
id = "unique"
|
||||
)
|
||||
)
|
||||
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults))
|
||||
|
|
@ -78,6 +78,7 @@ class DepictsPresenterTest {
|
|||
.assertValue(listOf(selectedItem, depictedItem(id="nonUnique")))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun `empty search results with empty term do not show error`() {
|
||||
whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(emptyList()))
|
||||
|
|
@ -137,15 +138,4 @@ class DepictsPresenterTest {
|
|||
depictsPresenter.verifyDepictions()
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import android.net.ConnectivityManager;
|
|||
import android.net.NetworkInfo;
|
||||
import android.telephony.TelephonyManager;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
|
|
@ -28,34 +29,30 @@ public class NetworkUtilsTest {
|
|||
|
||||
@Test
|
||||
public void testInternetConnectionEstablished() {
|
||||
Context mockContext = mock(Context.class);
|
||||
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);
|
||||
Context mockContext = getContext(true);
|
||||
boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext);
|
||||
assertTrue(internetConnectionEstablished);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternetConnectionNotEstablished() {
|
||||
@NotNull
|
||||
public static Context getContext(boolean connectionEstablished) {
|
||||
Context mockContext = mock(Context.class);
|
||||
Application mockApplication = mock(Application.class);
|
||||
ConnectivityManager mockConnectivityManager = mock(ConnectivityManager.class);
|
||||
NetworkInfo mockNetworkInfo = mock(NetworkInfo.class);
|
||||
when(mockNetworkInfo.isConnectedOrConnecting())
|
||||
.thenReturn(false);
|
||||
.thenReturn(connectionEstablished);
|
||||
when(mockConnectivityManager.getActiveNetworkInfo())
|
||||
.thenReturn(mockNetworkInfo);
|
||||
.thenReturn(mockNetworkInfo);
|
||||
when(mockApplication.getSystemService(Context.CONNECTIVITY_SERVICE))
|
||||
.thenReturn(mockConnectivityManager);
|
||||
.thenReturn(mockConnectivityManager);
|
||||
when(mockContext.getApplicationContext()).thenReturn(mockApplication);
|
||||
return mockContext;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInternetConnectionNotEstablished() {
|
||||
Context mockContext = getContext(false);
|
||||
boolean internetConnectionEstablished = NetworkUtils.isInternetConnectionEstablished(mockContext);
|
||||
assertFalse(internetConnectionEstablished);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue