mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Improve credit line in image list (#6295)
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
Some checks are pending
Android CI / Run tests and generate APK (push) Waiting to run
- When author is not uploader, show both. - When failing to parse author from HTML, use structured data.
This commit is contained in:
parent
30762971db
commit
329a68216e
21 changed files with 363 additions and 81 deletions
|
|
@ -333,6 +333,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 +371,7 @@ android {
|
|||
buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\""
|
||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
|
||||
buildConfigField "String", "CREATOR_PROPERTY", "\"P253075\""
|
||||
dimension 'tier'
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, String> = emptyMap(),
|
||||
var descriptions: Map<String, String> = emptyMap(),
|
||||
var depictionIds: List<String> = emptyList(),
|
||||
var creatorIds: List<String> = 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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -29,7 +29,17 @@ class MediaDataExtractor
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Contribution, ContributionViewHolder>(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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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 anchorStartTagTerminalString = "\">"
|
||||
val anchorCloseTag = "</a>"
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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<Media>(),
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<Media, SearchImagesViewHolder>(
|
||||
object : DiffUtil.ItemCallback<Media>() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
data class IdAndCaptions(
|
||||
val id: String,
|
||||
val captions: Map<String, String>,
|
||||
)
|
||||
18
app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
Normal file
18
app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
data class IdAndLabels(
|
||||
val id: String,
|
||||
val labels: Map<String, String>,
|
||||
) {
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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<IdAndCaptions> -> onDepictionsLoaded(idAndCaptions) },
|
||||
{ IdAndLabels: List<IdAndLabels> -> onDepictionsLoaded(IdAndLabels) },
|
||||
{ t: Throwable? -> Timber.e(t) })
|
||||
)
|
||||
// compositeDisposable.add(disposable);
|
||||
}
|
||||
|
||||
private fun onDiscussionLoaded(discussion: String) {
|
||||
|
|
@ -655,10 +653,10 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
|||
}
|
||||
}
|
||||
|
||||
private fun onDepictionsLoaded(idAndCaptions: List<IdAndCaptions>) {
|
||||
private fun onDepictionsLoaded(IdAndLabels: List<IdAndLabels>) {
|
||||
binding.depictsLayout.visibility = View.VISIBLE
|
||||
binding.depictionsEditButton.visibility = View.VISIBLE
|
||||
buildDepictionList(idAndCaptions)
|
||||
buildDepictionList(IdAndLabels)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -863,26 +861,26 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
|
|||
|
||||
/**
|
||||
* Populates media details fragment with depiction list
|
||||
* @param idAndCaptions
|
||||
* @param IdAndLabels
|
||||
*/
|
||||
private fun buildDepictionList(idAndCaptions: List<IdAndCaptions>) {
|
||||
private fun buildDepictionList(IdAndLabels: List<IdAndLabels>) {
|
||||
binding.mediaDetailDepictionContainer.removeAllViews()
|
||||
|
||||
// Create a mutable list from the original list
|
||||
val mutableIdAndCaptions = idAndCaptions.toMutableList()
|
||||
val mutableIdAndLabels = IdAndLabels.toMutableList()
|
||||
|
||||
if (mutableIdAndCaptions.isEmpty()) {
|
||||
// Create a placeholder IdAndCaptions object and add it to the list
|
||||
mutableIdAndCaptions.add(
|
||||
IdAndCaptions(
|
||||
if (mutableIdAndLabels.isEmpty()) {
|
||||
// Create a placeholder IdAndLabels object and add it to the list
|
||||
mutableIdAndLabels.add(
|
||||
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: IdAndLabels in mutableIdAndLabels) {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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<IdAndLabels>): 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(", ")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -873,4 +873,6 @@ Upload your first media by tapping on the add button.</string>
|
|||
|
||||
<string name="show_in_explore">Show in Explore</string>
|
||||
<string name="show_in_nearby">Show in Nearby</string>
|
||||
<string name="image_tag_line_created_and_uploaded_by">Created and uploaded by: %1$s</string>
|
||||
<string name="image_tag_line_created_by_and_uploaded_by">Created by %1$s and uploaded by %2$s</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ fun media(
|
|||
licenseUrl: String? = "licenseUrl",
|
||||
author: String? = "creator",
|
||||
user: String? = "user",
|
||||
creatorName: String? = null,
|
||||
pageId: String = "pageId",
|
||||
categories: List<String>? = listOf("categories"),
|
||||
coordinates: LatLng? = LatLng(0.0, 0.0, 0.0f),
|
||||
|
|
@ -67,6 +68,7 @@ fun media(
|
|||
licenseUrl,
|
||||
author,
|
||||
user,
|
||||
creatorName,
|
||||
categories,
|
||||
coordinates,
|
||||
captions,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import androidx.test.core.app.ApplicationProvider
|
|||
import com.facebook.drawee.backends.pipeline.Fresco
|
||||
import com.facebook.soloader.SoLoader
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.MediaDataExtractor
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.TestCommonsApplication
|
||||
import fr.free.nrw.commons.TestUtility.setFinalStatic
|
||||
|
|
@ -46,6 +47,9 @@ class ContributionViewHolderUnitTests {
|
|||
@Mock
|
||||
private lateinit var mediaClient: MediaClient
|
||||
|
||||
@Mock
|
||||
private lateinit var mediaDataExtractor: MediaDataExtractor
|
||||
|
||||
@Mock
|
||||
private lateinit var uri: Uri
|
||||
|
||||
|
|
@ -66,8 +70,9 @@ class ContributionViewHolderUnitTests {
|
|||
SoLoader.setInTestMode()
|
||||
Fresco.initialize(ApplicationProvider.getApplicationContext())
|
||||
activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get()
|
||||
compositeDisposable = CompositeDisposable()
|
||||
parent = LayoutInflater.from(activity).inflate(R.layout.layout_contribution, null)
|
||||
contributionViewHolder = ContributionViewHolder(parent, callback, mediaClient)
|
||||
contributionViewHolder = ContributionViewHolder(parent, callback, compositeDisposable, mediaClient, mediaDataExtractor)
|
||||
|
||||
bindind = LayoutContributionBinding.bind(parent)
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.junit.Before
|
|||
import org.junit.Test
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.MockitoAnnotations
|
||||
import java.lang.IllegalArgumentException
|
||||
|
||||
|
|
@ -42,23 +43,61 @@ class MediaConverterTest {
|
|||
|
||||
@Test
|
||||
fun testConvertIfThumbUrlBlank() {
|
||||
Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("")
|
||||
Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl")
|
||||
Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
`when`(imageInfo.getThumbUrl()).thenReturn("")
|
||||
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
|
||||
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(metadata.artist()).thenReturn("Foo Bar")
|
||||
media = mediaConverter.convert(page, entity, imageInfo)
|
||||
assertEquals(media.thumbUrl, media.imageUrl, "originalUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testConvertIfThumbUrlNotBlank() {
|
||||
Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
|
||||
Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl")
|
||||
Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
|
||||
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
|
||||
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(metadata.artist()).thenReturn("Foo Bar")
|
||||
media = mediaConverter.convert(page, entity, imageInfo)
|
||||
assertEquals(media.thumbUrl, "thumbUrl")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test converting artist value (author) with html links`() {
|
||||
`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
|
||||
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
|
||||
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(metadata.artist()).thenReturn("<a href=\"//commons.wikimedia.org/wiki/User:Foo_Bar\" title=\"Foo Bar\">Foo Bar</a>")
|
||||
// Artist values like above is very common, found in file pages created via UploadWizard
|
||||
media = mediaConverter.convert(page, entity, imageInfo)
|
||||
assertEquals("Foo Bar", media.author)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test convert artist value (author) in plain text`() {
|
||||
`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
|
||||
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
|
||||
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(metadata.artist()).thenReturn("Foo Bar")
|
||||
media = mediaConverter.convert(page, entity, imageInfo)
|
||||
assertEquals("Foo Bar", media.author)
|
||||
}
|
||||
@Test
|
||||
fun `test convert artist value (author) containing red link`() {
|
||||
`when`(imageInfo.getMetadata()).thenReturn(metadata)
|
||||
`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl")
|
||||
`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl")
|
||||
`when`(metadata.licenseUrl()).thenReturn("licenseUrl")
|
||||
`when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss")
|
||||
`when`(metadata.artist()).thenReturn("<a href=\"/w/index.php?title=User:Foo&action=edit&redlink=1\" class=\"new\" title=\"User:Foo (page does not exist)\">Foo</a>")
|
||||
media = mediaConverter.convert(page, entity, imageInfo)
|
||||
assertEquals("Foo", media.author)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package fr.free.nrw.commons.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.TestCommonsApplication
|
||||
import fr.free.nrw.commons.media.IdAndLabels
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.mock
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [21], application = TestCommonsApplication::class, qualifiers="en-rUS")
|
||||
class MediaAttributionUtilTest {
|
||||
|
||||
@Mock
|
||||
private lateinit var appContext: Context
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
appContext = ApplicationProvider.getApplicationContext()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getTagLineWithUploaderOnly() {
|
||||
val media = mock(Media::class.java)
|
||||
whenever(media.user).thenReturn("TestUploader")
|
||||
whenever(media.author).thenReturn(null)
|
||||
assertEquals("Uploaded by: TestUploader",
|
||||
MediaAttributionUtil.getTagLine(media, appContext))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get tag line from same author and uploader`() {
|
||||
val media = mock(Media::class.java)
|
||||
whenever(media.user).thenReturn("TestUser")
|
||||
whenever(media.getAttributedAuthor()).thenReturn("TestUser")
|
||||
assertEquals("Created and uploaded by: TestUser",
|
||||
MediaAttributionUtil.getTagLine(media, appContext))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get creator name from EN label`() {
|
||||
assertEquals("FooBar",
|
||||
MediaAttributionUtil.getCreatorName(listOf(IdAndLabels("Q1", mapOf("en" to "FooBar")))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get creator name from ES label`() {
|
||||
assertEquals("FooBar",
|
||||
MediaAttributionUtil.getCreatorName(listOf(IdAndLabels("Q2", mapOf("es" to "FooBar")))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get creator name from EN label and ignore ES label`() {
|
||||
assertEquals("Bar",
|
||||
MediaAttributionUtil.getCreatorName(listOf(
|
||||
IdAndLabels("Q3", mapOf("en" to "Bar", "es" to "Foo")))))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get creator name from two creators`() {
|
||||
val name = MediaAttributionUtil.getCreatorName(listOf(
|
||||
IdAndLabels("Q1", mapOf("en" to "Foo")),
|
||||
IdAndLabels("Q1", mapOf("en" to "Bar"))
|
||||
))
|
||||
assertNotNull(name)
|
||||
assertTrue(name!!.contains("Foo"))
|
||||
assertTrue(name.contains("Bar"))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue