diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0da417cfa9..df55b1124f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,15 @@
# Wikimedia Commons for Android
+## v5.3.0
+
+### What's changed
+* Enable EmailAuth support
+* Explore map images no longer show "Unknown"
+* Fix crash when removing last two images of multiupload
+* Mark ❌ for closed locations (P3999) in Nearby
+* Fix two pin labels staying visible at the same time in Explore map
+* Refactoring and minor UI improvements
+
## v5.2.0
v5.2.0 boasts several new functionalities like:
diff --git a/app/build.gradle b/app/build.gradle
index 133d39b6ea..34dd7f0081 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -119,7 +119,8 @@ dependencies {
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0"
testImplementation 'com.facebook.soloader:soloader:0.10.5'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3"
- debugImplementation("androidx.fragment:fragment-testing:1.6.2")
+ debugImplementation("androidx.fragment:fragment-testing-manifest:1.6.2")
+ androidTestImplementation("androidx.fragment:fragment-testing:1.6.2")
testImplementation "commons-io:commons-io:2.6"
// Android testing
@@ -212,8 +213,8 @@ android {
defaultConfig {
//applicationId 'fr.free.nrw.commons'
- versionCode 1050
- versionName '5.3.0'
+ versionCode 1051
+ versionName '5.4.0'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion 21
@@ -333,6 +334,7 @@ android {
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
+ buildConfigField "String", "CREATOR_PROPERTY", "\"P170\""
dimension 'tier'
}
@@ -370,6 +372,7 @@ android {
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
+ buildConfigField "String", "CREATOR_PROPERTY", "\"P253075\""
dimension 'tier'
}
}
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt b/app/src/androidTest/java/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt
similarity index 100%
rename from app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt
rename to app/src/androidTest/java/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt b/app/src/androidTest/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt
similarity index 100%
rename from app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt
rename to app/src/androidTest/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt
diff --git a/app/src/main/java/fr/free/nrw/commons/Media.kt b/app/src/main/java/fr/free/nrw/commons/Media.kt
index 1222cd8b0e..d07bc0265e 100644
--- a/app/src/main/java/fr/free/nrw/commons/Media.kt
+++ b/app/src/main/java/fr/free/nrw/commons/Media.kt
@@ -53,6 +53,7 @@ class Media constructor(
*/
var author: String? = null,
var user: String? = null,
+ var creatorName: String? = null,
/**
* Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings
@@ -66,6 +67,7 @@ class Media constructor(
var captions: Map = emptyMap(),
var descriptions: Map = emptyMap(),
var depictionIds: List = emptyList(),
+ var creatorIds: List = emptyList(),
/**
* This field was added to find non-hidden categories
* Stores the mapping of category title to hidden attribute
@@ -130,6 +132,7 @@ class Media constructor(
* returns user
* @return Author or User
*/
+ @Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.")
fun getAuthorOrUser(): String? {
return if (!author.isNullOrEmpty()) {
author
@@ -138,6 +141,19 @@ class Media constructor(
}
}
+ /**
+ * Returns author if it's not null or empty, otherwise
+ * returns creator name
+ * @return name of author or creator
+ */
+ fun getAttributedAuthor(): String? {
+ return if (!author.isNullOrEmpty()) {
+ author
+ } else{
+ creatorName
+ }
+ }
+
/**
* Gets media display title
* @return Media title
diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt
index 2ff54959db..970413283b 100644
--- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt
+++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt
@@ -1,7 +1,7 @@
package fr.free.nrw.commons
import androidx.core.text.HtmlCompat
-import fr.free.nrw.commons.media.IdAndCaptions
+import fr.free.nrw.commons.media.IdAndLabels
import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import io.reactivex.Single
@@ -23,13 +23,23 @@ class MediaDataExtractor
private val mediaClient: MediaClient,
) {
fun fetchDepictionIdsAndLabels(media: Media) =
- mediaClient
+ mediaClient
.getEntities(media.depictionIds)
.map {
it
.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
- }.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
+ }.map { it.map { (key, value) -> IdAndLabels(key, value) } }
+ .onErrorReturn { emptyList() }
+
+ fun fetchCreatorIdsAndLabels(media: Media) =
+ mediaClient
+ .getEntities(media.creatorIds)
+ .map {
+ it
+ .entities()
+ .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
+ }.map { it.map { (key, value) -> IdAndLabels(key, value) } }
.onErrorReturn { emptyList() }
fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
diff --git a/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java
index 5ca20372ab..b887aaf99e 100644
--- a/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java
+++ b/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java
@@ -18,6 +18,17 @@ public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
+ /**
+ * Constructs a ViewPagerAdapter with a specified Fragment Manager and Fragment resume behavior.
+ *
+ * @param manager The FragmentManager
+ * @param behavior An integer which represents the behavior of non visible fragments. See
+ * FragmentPagerAdapter.java for options.
+ */
+ public ViewPagerAdapter(FragmentManager manager, int behavior) {
+ super(manager, behavior);
+ }
+
/**
* This method returns the fragment of the viewpager at a particular position
* @param position
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt
index 32028cfd2a..899ef458f6 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt
@@ -8,23 +8,29 @@ import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import com.facebook.imagepipeline.request.ImageRequestBuilder
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.utils.MediaAttributionUtil
+import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.LayoutContributionBinding
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
import java.io.File
class ContributionViewHolder internal constructor(
- private val parent: View, private val callback: ContributionsListAdapter.Callback,
- private val mediaClient: MediaClient
+ parent: View,
+ private val callback: ContributionsListAdapter.Callback,
+ private val compositeDisposable: CompositeDisposable,
+ private val mediaClient: MediaClient,
+ private val mediaDataExtractor: MediaDataExtractor
) : RecyclerView.ViewHolder(parent) {
var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent)
private var position = 0
private var contribution: Contribution? = null
- private val compositeDisposable = CompositeDisposable()
private var isWikipediaButtonDisplayed = false
private val pausingPopUp: AlertDialog
var imageRequest: ImageRequest? = null
@@ -54,7 +60,7 @@ an upload might take a dozen seconds. */
this.contribution = contribution
this.position = position
binding.contributionTitle.text = contribution.media.mostRelevantCaption
- binding.authorView.text = contribution.media.getAuthorOrUser()
+ setAuthorText(contribution.media)
//Removes flicker of loading image.
binding.contributionImage.hierarchy.fadeDuration = 0
@@ -93,6 +99,30 @@ an upload might take a dozen seconds. */
checkIfMediaExistsOnWikipediaPage(contribution)
}
+ fun updateAttribution() {
+ if (contribution != null) {
+ val media = contribution!!.media
+ if (!media.getAttributedAuthor().isNullOrEmpty()) {
+ return
+ }
+ compositeDisposable.addAll(
+ mediaDataExtractor.fetchCreatorIdsAndLabels(media)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { idAndLabels ->
+ media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels)
+ setAuthorText(media)
+ },
+ { t: Throwable? -> Timber.e(t) })
+ )
+ }
+ }
+
+ private fun setAuthorText(media: Media) {
+ binding.authorView.text = MediaAttributionUtil.getTagLine(media, itemView.context)
+ }
+
/**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
* for the device's current language Wikipedia
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt
index b41de1c6e6..e5f721721e 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt
@@ -4,21 +4,26 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
+import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.media.MediaClient
+import io.reactivex.disposables.CompositeDisposable
/**
* Represents The View Adapter for the List of Contributions
*/
class ContributionsListAdapter internal constructor(
private val callback: Callback,
- private val mediaClient: MediaClient
+ private val mediaClient: MediaClient,
+ private val mediaDataExtractor: MediaDataExtractor,
+ private val compositeDisposable: CompositeDisposable
) : PagedListAdapter(DIFF_CALLBACK) {
/**
* Initializes the view holder with contribution data
*/
override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) {
holder.init(position, getItem(position))
+ holder.updateAttribution()
}
fun getContributionForPosition(position: Int): Contribution? {
@@ -36,7 +41,7 @@ class ContributionsListAdapter internal constructor(
val viewHolder = ContributionViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.layout_contribution, parent, false),
- callback, mediaClient
+ callback, compositeDisposable, mediaClient, mediaDataExtractor
)
return viewHolder
}
diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt
index bfe1161c7a..9ecb35b249 100644
--- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt
@@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver
import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener
import androidx.recyclerview.widget.SimpleItemAnimator
import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.auth.SessionManager
@@ -63,6 +64,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
@Inject
var mediaClient: MediaClient? = null
+ @JvmField
+ @Inject
+ var mediaDataExtractor: MediaDataExtractor? = null
+
@JvmField
@Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
@Inject
@@ -231,7 +236,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL
}
private fun initAdapter() {
- adapter = ContributionsListAdapter(this, mediaClient!!)
+ adapter = ContributionsListAdapter(this, mediaClient!!, mediaDataExtractor!!, compositeDisposable)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt
index ff623d4960..20a2fe70a0 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt
@@ -26,6 +26,7 @@ import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.util.TreeMap
import kotlin.collections.ArrayList
@@ -342,45 +343,36 @@ class ImageAdapter(
numberOfSelectedImagesMarkedAsNotForUpload--
}
notifyItemChanged(position, ImageUnselected())
-
- // Getting index from all images index when switch is on
- val indexes =
- if (showAlreadyActionedImages) {
- ImageHelper.getIndexList(selectedImages, images)
-
- // Getting index from actionable images when switch is off
- } else {
- ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
- }
- for (index in indexes) {
- notifyItemChanged(index, ImageSelectedOrUpdated())
- }
} else {
- if (holder.isItemUploaded()) {
- Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
- } else {
- if (holder.isItemNotForUpload()) {
- numberOfSelectedImagesMarkedAsNotForUpload++
- }
+ val image = images[position]
+ scope.launch(ioDispatcher) {
+ val imageSHA1 = imageLoader.getSHA1(image, defaultDispatcher)
+ withContext(Dispatchers.Main) {
+ if (holder.isItemUploaded()) {
+ Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
+ return@withContext
+ }
+
+ if (imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) {
+ holder.itemUploaded()
+ Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
+ return@withContext
+ }
- // Getting index from all images index when switch is on
- val indexes: ArrayList =
- if (showAlreadyActionedImages) {
- selectedImages.add(images[position])
- ImageHelper.getIndexList(selectedImages, images)
+ if (!holder.isItemUploaded() && imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) {
+ Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show()
+ }
- // Getting index from actionable images when switch is off
- } else {
- selectedImages.add(ArrayList(actionableImagesMap.values)[position])
- ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values))
+ if (holder.isItemNotForUpload()) {
+ numberOfSelectedImagesMarkedAsNotForUpload++
}
+ selectedImages.add(image)
+ notifyItemChanged(position, ImageSelectedOrUpdated())
- for (index in indexes) {
- notifyItemChanged(index, ImageSelectedOrUpdated())
+ imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
}
}
}
- imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload)
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
index 4e2d58babe..6b78dfd413 100644
--- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
+++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt
@@ -638,17 +638,20 @@ class CustomSelectorActivity :
finishPickImages(arrayListOf())
return
}
- var i = 0
- while (i < selectedImages.size) {
- val path = selectedImages[i].path
- val file = File(path)
- if (!file.exists()) {
- selectedImages.removeAt(i)
- i--
+ scope.launch(ioDispatcher) {
+ val uniqueImages = selectedImages.distinctBy { image ->
+ CustomSelectorUtils.getImageSHA1(
+ image.uri,
+ ioDispatcher,
+ fileUtilsWrapper,
+ contentResolver
+ )
+ }
+
+ withContext(Dispatchers.Main) {
+ finishPickImages(ArrayList(uniqueImages))
}
- i++
}
- finishPickImages(selectedImages)
}
/**
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java
index 223d028dc5..b31c34b67a 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java
@@ -12,6 +12,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
@@ -69,7 +70,9 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
loadNearbyMapData();
binding = FragmentExploreBinding.inflate(inflater, container, false);
- viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager());
+ viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager(),
+ FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
+
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setId(R.id.viewPager);
binding.tabLayout.setupWithViewPager(binding.viewPager);
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java
index 60758ac201..feebd20c6c 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java
@@ -221,7 +221,6 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved
binding.mapView.getController().setZoom(ZOOM_LEVEL);
}
- performMapReadyActions();
binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() {
@Override
@@ -341,7 +340,12 @@ private void performMapReadyActions() {
!locationPermissionsHelper.checkLocationPermission(getActivity())) {
isPermissionDenied = true;
}
- lastKnownLocation = MapUtils.getDefaultLatLng();
+
+ lastKnownLocation = getLastLocation();
+
+ if (lastKnownLocation == null) {
+ lastKnownLocation = MapUtils.getDefaultLatLng();
+ }
// if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom
if (isCameFromNearbyMap()) {
@@ -717,8 +721,20 @@ private void addMarkerToMap(BaseMarker nearbyBaseMarker) {
authorUser = Html.fromHtml(authorUser, Html.FROM_HTML_MODE_LEGACY).toString();
}
- OverlayItem item = new OverlayItem(nearbyBaseMarker.getPlace().name,
- authorUser, point);
+ String title = nearbyBaseMarker.getPlace().name;
+ // Remove "File:" if present at start
+ if (title.startsWith("File:")) {
+ title = title.substring(5);
+ }
+ // Remove extensions like .jpg, .jpeg, .png, .svg (case insensitive)
+ title = title.replaceAll("(?i)\\.(jpg|jpeg|png|svg)$", "");
+ title = title.replace("_", " ");
+ //Truncate if too long because it doesn't fit the screen
+ if (title.length() > 43) {
+ title = title.substring(0, 40) + "…";
+ }
+
+ OverlayItem item = new OverlayItem(title, authorUser, point);
item.setMarker(d);
items.add(item);
ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items,
@@ -962,9 +978,6 @@ public fr.free.nrw.commons.location.LatLng getMapCenter() {
-0.07483536015053005, 1f);
}
}
- if (!isCameFromNearbyMap()) {
- moveCameraToPosition(new GeoPoint(latLnge.getLatitude(), latLnge.getLongitude()));
- }
return latLnge;
}
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt
index 0cfb270a31..fe5c21a7e1 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt
@@ -18,6 +18,12 @@ import javax.inject.Inject
class MediaConverter
@Inject
constructor() {
+ /**
+ * Creating Media object from MWQueryPage.
+ *
+ * @param page response from the API
+ * @return Media object
+ */
fun convert(
page: MwQueryPage,
entity: Entities.Entity,
@@ -40,24 +46,17 @@ class MediaConverter
metadata.prefixedLicenseUrl,
getAuthor(metadata),
imageInfo.getUser(),
+ null,
MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()),
metadata.latLng,
entity.labels().mapValues { it.value.value() },
entity.descriptions().mapValues { it.value.value() },
entity.depictionIds(),
+ entity.creatorIds(),
myMap,
)
}
- /**
- * Creating Media object from MWQueryPage.
- * Earlier only basic details were set for the media object but going forward,
- * a full media object(with categories, descriptions, coordinates etc) can be constructed using this method
- *
- * @param page response from the API
- * @return Media object
- */
-
private fun safeParseDate(dateStr: String): Date? =
try {
CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr)
@@ -66,24 +65,32 @@ class MediaConverter
}
/**
- * This method extracts the Commons Username from the artist HTML information
+ * This method extracts the Commons Username from the artist HTML information.
+ * When the HTML is in customized formatting, it may fail to parse and return null.
* @param metadata
* @return
*/
private fun getAuthor(metadata: ExtMetadata): String? {
- return try {
- val authorHtml = metadata.artist()
- val anchorStartTagTerminalChars = "\">"
- val anchorCloseTag = ""
+ val authorHtml = metadata.artist()
+ val anchorStartTagTerminalString = "\">"
+ val anchorCloseTag = ""
- return authorHtml.substring(
- authorHtml.indexOf(anchorStartTagTerminalChars) +
- anchorStartTagTerminalChars
- .length,
+ return if (!authorHtml.contains("<") && !authorHtml.contains(">") ) {
+ authorHtml.trim()
+ } else if (!authorHtml.contains(anchorStartTagTerminalString) || !authorHtml.endsWith(anchorCloseTag)) {
+ null
+ } else {
+
+ val authorText = authorHtml.substring(
+ authorHtml.indexOf(anchorStartTagTerminalString) +
+ anchorStartTagTerminalString.length,
authorHtml.indexOf(anchorCloseTag),
)
- } catch (ex: java.lang.Exception) {
- ""
+ if (authorText.contains("<") || authorText.contains(">")) {
+ null
+ } else {
+ authorText
+ }
}
}
}
@@ -92,6 +99,10 @@ private fun Entities.Entity.depictionIds() =
this[WikidataProperties.DEPICTS]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id }
?: emptyList()
+private fun Entities.Entity.creatorIds() =
+ this[WikidataProperties.CREATOR]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id }
+ ?: emptyList()
+
private val ExtMetadata.prefixedLicenseUrl: String
get() =
licenseUrl().let {
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt
index 987f4ca005..e19b1b056e 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt
@@ -4,16 +4,18 @@ import android.content.Context
import android.os.Bundle
import android.view.View
import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.MediaDataExtractor
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.explore.paging.BasePagingFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
+import javax.inject.Inject
abstract class PageableMediaFragment :
BasePagingFragment(),
MediaDetailProvider {
override val pagedListAdapter by lazy {
- PagedMediaAdapter(categoryImagesCallback::onMediaClicked)
+ PagedMediaAdapter(categoryImagesCallback::onMediaClicked, mediaDataExtractor)
}
override val errorTextId: Int = R.string.error_loading_images
@@ -22,6 +24,9 @@ abstract class PageableMediaFragment :
lateinit var categoryImagesCallback: CategoryImagesCallback
+ @Inject
+ lateinit var mediaDataExtractor: MediaDataExtractor
+
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment != null) {
diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt
index 364b5d3637..521ba77c66 100644
--- a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt
+++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt
@@ -5,13 +5,22 @@ import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.MediaDataExtractor
+import fr.free.nrw.commons.utils.MediaAttributionUtil
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.LayoutCategoryImagesBinding
import fr.free.nrw.commons.explore.paging.BaseViewHolder
import fr.free.nrw.commons.explore.paging.inflate
+import fr.free.nrw.commons.media.IdAndLabels
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.disposables.CompositeDisposable
+import io.reactivex.schedulers.Schedulers
+import timber.log.Timber
class PagedMediaAdapter(
private val onImageClicked: (Int) -> Unit,
+ private val mediaDataExtractor: MediaDataExtractor,
+ private val compositeDisposable: CompositeDisposable = CompositeDisposable()
) : PagedListAdapter(
object : DiffUtil.ItemCallback() {
override fun areItemsTheSame(
@@ -25,6 +34,7 @@ class PagedMediaAdapter(
) = oldItem.pageId == newItem.pageId
},
) {
+
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
@@ -37,7 +47,24 @@ class PagedMediaAdapter(
holder: SearchImagesViewHolder,
position: Int,
) {
- holder.bind(getItem(position)!! to position)
+ val media = getItem(position) ?: return
+ holder.bind(media to position)
+
+ if (!media.getAttributedAuthor().isNullOrEmpty()) {
+ return
+ }
+
+ compositeDisposable.addAll(
+ mediaDataExtractor.fetchCreatorIdsAndLabels(media)
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(
+ { idAndLabels ->
+ media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels);
+ holder.setAuthorText(media)
+ },
+ { t: Throwable? -> Timber.e(t) })
+ )
}
}
@@ -52,7 +79,10 @@ class SearchImagesViewHolder(
binding.categoryImageView.setOnClickListener { onImageClicked(item.second) }
binding.categoryImageTitle.text = media.mostRelevantCaption
binding.categoryImageView.setImageURI(media.thumbUrl)
- binding.categoryImageAuthor.text =
- containerView.context.getString(R.string.image_uploaded_by, media.getAuthorOrUser())
+ setAuthorText(media)
+ }
+
+ fun setAuthorText(media: Media) {
+ binding.categoryImageAuthor.text = MediaAttributionUtil.getTagLine(media, containerView.context)
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt
deleted file mode 100644
index fe96eb8cb0..0000000000
--- a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package fr.free.nrw.commons.media
-
-data class IdAndCaptions(
- val id: String,
- val captions: Map,
-)
diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
new file mode 100644
index 0000000000..c989ee7e3e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.media
+
+data class IdAndLabels(
+ val id: String,
+ val labels: Map,
+) {
+ // if a label is available in user's locale, return it
+ // if not then check for english, else show any available.
+ fun getLocalizedLabel(locale: String): String? {
+ if (labels[locale] != null) {
+ return labels[locale]
+ }
+ if (labels["en"] != null) {
+ return labels["en"]
+ }
+ return labels.values.firstOrNull() ?: id
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
index 77ff1df0cc..8a4d530c47 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
@@ -16,7 +16,6 @@ import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
-import android.view.ViewTreeObserver
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.ArrayAdapter
import android.widget.Button
@@ -622,10 +621,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
- { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) },
+ { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) },
{ t: Throwable? -> Timber.e(t) })
)
- // compositeDisposable.add(disposable);
}
private fun onDiscussionLoaded(discussion: String) {
@@ -655,7 +653,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
}
- private fun onDepictionsLoaded(idAndCaptions: List) {
+ private fun onDepictionsLoaded(idAndCaptions: List) {
binding.depictsLayout.visibility = View.VISIBLE
binding.depictionsEditButton.visibility = View.VISIBLE
buildDepictionList(idAndCaptions)
@@ -865,24 +863,24 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
* Populates media details fragment with depiction list
* @param idAndCaptions
*/
- private fun buildDepictionList(idAndCaptions: List) {
+ private fun buildDepictionList(idAndCaptions: List) {
binding.mediaDetailDepictionContainer.removeAllViews()
// Create a mutable list from the original list
val mutableIdAndCaptions = idAndCaptions.toMutableList()
if (mutableIdAndCaptions.isEmpty()) {
- // Create a placeholder IdAndCaptions object and add it to the list
+ // Create a placeholder IdAndLabels object and add it to the list
mutableIdAndCaptions.add(
- IdAndCaptions(
+ IdAndLabels(
id = media?.pageId ?: "", // Use an empty string if media?.pageId is null
- captions = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
+ labels = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value
)
)
}
val locale: String = Locale.getDefault().language
- for (idAndCaption: IdAndCaptions in mutableIdAndCaptions) {
+ for (idAndCaption in mutableIdAndCaptions) {
binding.mediaDetailDepictionContainer.addView(
buildDepictLabel(
getDepictionCaption(idAndCaption, locale),
@@ -894,16 +892,16 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
}
- private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? {
+ private fun getDepictionCaption(idAndCaption: IdAndLabels, locale: String): String? {
// Check if the Depiction Caption is available in user's locale
// if not then check for english, else show any available.
- if (idAndCaption.captions[locale] != null) {
- return idAndCaption.captions[locale]
+ if (idAndCaption.labels[locale] != null) {
+ return idAndCaption.labels[locale]
}
- if (idAndCaption.captions["en"] != null) {
- return idAndCaption.captions["en"]
+ if (idAndCaption.labels["en"] != null) {
+ return idAndCaption.labels["en"]
}
- return idAndCaption.captions.values.iterator().next()
+ return idAndCaption.labels.values.iterator().next()
}
private fun onMediaDetailLicenceClicked() {
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
index ef0ef1f9ca..643374e54c 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt
@@ -186,13 +186,25 @@ interface MediaInterface {
): Single
companion object {
+ /**
+ * Retrieved thumbnail height will be about this tall, but must be at least this height.
+ * A larger number means higher thumbnail resolution but more network usage.
+ */
+ const val THUMB_HEIGHT_PX = 450
+
const val MEDIA_PARAMS =
- "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
+ "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlheight=" +
+ THUMB_HEIGHT_PX +
+ "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|" +
+ "ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
/**
* fetches category detail(title, hidden) for each category along with File information
*/
const val MEDIA_PARAMS_WITH_CATEGORY_DETAILS =
- "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
+ "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlheight=" +
+ THUMB_HEIGHT_PX +
+ "&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|" +
+ "DateTimeOriginal|Artist|LicenseShortName|LicenseUrl"
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
index e5196bee89..a1bad1f268 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt
@@ -103,4 +103,4 @@ class WikidataFeedback : BaseActivity() {
onBackPressed()
return true
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt
index 25baf3a92e..7445a65265 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt
@@ -1064,7 +1064,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
override fun updateListFragment(placeList: List) {
adapter!!.clear()
- adapter!!.items = placeList
+ adapter!!.items = placeList.filter{ it.name.isNotEmpty() }
binding!!.bottomSheetNearby.noResultsMessage.visibility =
if (placeList.isEmpty()) View.VISIBLE else View.GONE
}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaAttributionUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaAttributionUtil.kt
new file mode 100644
index 0000000000..7a66a87def
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaAttributionUtil.kt
@@ -0,0 +1,39 @@
+package fr.free.nrw.commons.utils
+
+import android.content.Context
+import android.icu.text.ListFormatter
+import android.os.Build
+import fr.free.nrw.commons.Media
+import fr.free.nrw.commons.R
+import fr.free.nrw.commons.media.IdAndLabels
+import java.util.Locale
+
+object MediaAttributionUtil {
+ fun getTagLine(media: Media, context: Context): String {
+ val uploader = media.user
+ val author = media.getAttributedAuthor()
+ return if (author.isNullOrEmpty()) {
+ context.getString(R.string.image_uploaded_by, uploader)
+ } else if (author == uploader) {
+ context.getString(R.string.image_tag_line_created_and_uploaded_by, author)
+ } else {
+ context.getString(
+ R.string.image_tag_line_created_by_and_uploaded_by,
+ author,
+ uploader
+ )
+ }
+ }
+
+ fun getCreatorName(idAndLabels: List): String? {
+ val locale = Locale.getDefault()
+ val names = idAndLabels.map{ x -> x.getLocalizedLabel(locale.language)}
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val formatter = ListFormatter.getInstance(locale)
+ return formatter.format(names)
+ } else {
+ return names.joinToString(", ")
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt
index b3c58d8b29..59c6aea979 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt
+++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt
@@ -1,9 +1,8 @@
package fr.free.nrw.commons.utils
-import android.os.Build
-import android.text.Html
import android.text.Spanned
import android.text.SpannedString
+import androidx.core.text.HtmlCompat
object StringUtil {
@@ -26,12 +25,6 @@ object StringUtil {
.replace("", "\u200F")
.replace("&", "&")
- return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
- Html.fromHtml(processedSource, Html.FROM_HTML_MODE_LEGACY)
- } else {
- //noinspection deprecation
- @Suppress("DEPRECATION")
- Html.fromHtml(processedSource)
- }
+ return HtmlCompat.fromHtml(processedSource, HtmlCompat.FROM_HTML_MODE_LEGACY)
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt
index 5e82c3c809..c4b95d0c55 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt
@@ -7,6 +7,7 @@ enum class WikidataProperties(
) {
IMAGE("P18"),
DEPICTS(BuildConfig.DEPICTS_PROPERTY),
+ CREATOR(BuildConfig.CREATOR_PROPERTY),
COMMONS_CATEGORY("P373"),
INSTANCE_OF("P31"),
MEDIA_LEGENDS("P2096"),
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt
index 63c0182520..53fc44bc96 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt
@@ -1,7 +1,6 @@
package fr.free.nrw.commons.wikidata.model.gallery
import com.google.gson.annotations.SerializedName
-import org.apache.commons.lang3.StringUtils
class ExtMetadata {
@SerializedName("DateTime") private val dateTime: Values? = null
diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt
index 492e2e1f82..7645e438d9 100644
--- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt
+++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt
@@ -72,7 +72,6 @@ open class ImageInfo : Serializable {
}
fun getThumbUrl(): String {
- updateThumbUrl()
return thumbUrl ?: ""
}
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 5c9e7bec98..9122f47674 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -889,4 +889,6 @@
مبروك، جميع الصور الموجودة في هذا الألبوم تم تحميلها أو تم وضع علامة عليها بأنها غير قابلة للتحميل.
عرض في استكشاف
عرض في المناطق القريبة
+ تم الإنشاء والتحميل بواسطة: %1$s
+ تم إنشاؤه بواسطة %1$s وتم تحميله بواسطة %2$s
diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml
index a7a3da9a77..9b2fde3133 100644
--- a/app/src/main/res/values-bn/strings.xml
+++ b/app/src/main/res/values-bn/strings.xml
@@ -2,6 +2,7 @@