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 @@