Merge branch 'main' into fix-multiupload

This commit is contained in:
Rohit Verma 2025-01-18 22:26:32 +05:30
commit 065fbd511d
39 changed files with 777 additions and 341 deletions

View file

@ -1,5 +1,89 @@
# Wikimedia Commons for Android # Wikimedia Commons for Android
## v5.1.2
### What's changed
* Fix the broken category search in the explore screen
## v5.1.1
### What's changed
* Use Android's new EXIF interface to mitigate security issues in old
EXIF interface.
* Make the icon that helps view the upload queue always visible as it ensures
that the queue accessible at all times.
## v5.1.0
### What's Changed
* Enhanced **upload queue management** in the Commons app for smoother, sequential
processing, clearer progress tracking, prevention of stuck or duplicate
uploads. As part of this improvement, the "Limited Connection mode" has been
removed.
* Added an option in "Nearby" feature enabling users to **provide feedback on
Wikidata items**. Users can report if an item doesnt exist, is at a different
location, or has other issues, with submissions tagged for easy tracking and
updates.
* Improved the "Nearby" feature by splitting the query into two parts for faster
loading and **better performance, especially in areas with dense amount of
places**. This update also resolves issues with pins overlapping place names.
* Upgraded AGP and **target/compile SDK to 34** and make necessary adjustments to
the app such as adding **"Partial Access" support**. Also includes some minor
refactoring, and replacement of deprecated circular progress bars.
* Fixed an **UI issue where the 'Subcategories' and 'Parent Categories' tabs
appeared blank** in the Category Details screen. Resolved by optimizing view
binding handling in the parent fragments.
* Fixed an issue where editing depictions removed all other structured data from
images. Now, **only depictions are updated, preserving other associated data**.
* Fixed **map centering** in the image upload flow to **use GPS EXIF tag location**
from pictures and ensured "Show in map app" accurately reflects this location.
* Fixed navigation **after uploading via Nearby by directing users to the Uploads
activity** instead of returning to Nearby, preventing confusion about needing to
upload again.
### Bug fixes and various changes
* Improved the "Nearby" feature to fetch labels based on the user's preferred
language instead of defaulting to English.
* Added a legend to the "Nearby" feature indicating pin statuses: red for items
without pictures, green for those with pictures, and grey for items being
checked. A floating action button now allows users to toggle the legend's
visibility.
* Fixed an issue where the "Nominate for deletion" option is shown to logged out
users, preventing app errors and crashes.
* Updated the regex pattern that filters categories with an year in it to also
filter the 2020s.
* Fix an issue where past depictions were not shown as suggestions, despite
being saved correctly.
* Fixed an issue in custom image picker where exiting the media preview showed
only the first image and cleared selections. Now, previously selected images
are restored correctly after exiting the preview. This was contributed.
* Fixed an issue in custom image picker where scrolling behavior did not
maintain position after exiting fullscreen preview, ensuring users remain at
the same point in their image roll unless actioned images are filtered. This
was contributed.
* Fixed Nearby map not showing new pins on map move by removing the 2000m scroll
threshold and adding an 800ms debounce for smoother pin updates when the map
is moved. Queued searches are now canceled on fragment destruction.
* Revised author information retrieval to emphasize the custom author name from
the metadata instead of the default registered username.
* Enhanced notification classification to properly identify "email" type
notifications and prompting users to check their e-mail inbox when such
notifications are clicked.
* Resolved a bug in the language chooser that incorrectly greyed-out previously
selected languages, ensuring only the current language is non-selectable during
image upload.
* Resolved pin color update issue in "Nearby" feature where the pin colour
failed to be updated after a successful image upload.
What's listed here is only a subset of all the changes. Check the full-list of
the changes in [this link](https://github.com/commons-app/apps-android-commons/compare/v5.0.2...v5.1.0).
Alternatively, checkout [this release on GitHub releases page](https://github.com/commons-app/apps-android-commons/releases/tag/v5.1.0)
for an exhaustive list of changes and the various contributors who contributed the same.
## v5.0.2 ## v5.0.2
- Enhanced multi-upload functionality with user prompts to clarify that all images would share the - Enhanced multi-upload functionality with user prompts to clarify that all images would share the

View file

@ -212,8 +212,8 @@ android {
defaultConfig { defaultConfig {
//applicationId 'fr.free.nrw.commons' //applicationId 'fr.free.nrw.commons'
versionCode 1040 versionCode 1043
versionName '5.0.2' versionName '5.1.2'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion 21 minSdkVersion 21
@ -314,6 +314,7 @@ android {
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\"" buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.org\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.org/wiki/\""
buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"" buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
@ -350,6 +351,7 @@ android {
buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\"" buildConfigField "String", "COMMONS_URL", "\"https://commons.wikimedia.beta.wmflabs.org\""
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\"" buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\"" buildConfigField "String", "MOBILE_HOME_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "MOBILE_META_URL", "\"https://meta.m.wikimedia.beta.wmflabs.org/wiki/\""
buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\"" buildConfigField "String", "SIGNUP_LANDING_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Special:CreateAccount&returnto=Main+Page&returntoquery=welcome%3Dyes\""
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\"" buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"" buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""

View file

@ -262,4 +262,4 @@
android:required="false" /> android:required="false" />
</application> </application>
</manifest> </manifest>

View file

@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.webkit.ConsoleMessage import android.webkit.ConsoleMessage
import android.webkit.CookieManager
import android.webkit.WebChromeClient import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebView import android.webkit.WebView
@ -28,13 +29,20 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.di.ApplicationlessInjection
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar
import okhttp3.HttpUrl.Companion.toHttpUrl
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject
/** /**
* SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and * SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and
* closes itself when a specified success URL is reached to success url. * closes itself when a specified success URL is reached to success url.
*/ */
class SingleWebViewActivity : ComponentActivity() { class SingleWebViewActivity : ComponentActivity() {
@Inject
lateinit var cookieJar: CommonsCookieJar
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -44,6 +52,11 @@ class SingleWebViewActivity : ComponentActivity() {
finish() finish()
return return
} }
ApplicationlessInjection
.getInstance(applicationContext)
.commonsApplicationComponent
.inject(this)
setCookies(url)
enableEdgeToEdge() enableEdgeToEdge()
setContent { setContent {
Scaffold( Scaffold(
@ -131,6 +144,7 @@ class SingleWebViewActivity : ComponentActivity() {
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url) super.onPageFinished(view, url)
setCookies(url.orEmpty())
} }
} }
@ -152,6 +166,20 @@ class SingleWebViewActivity : ComponentActivity() {
} }
/**
* Sets cookies for the given URL using the cookies stored in the `CommonsCookieJar`.
*
* @param url The URL for which cookies need to be set.
*/
private fun setCookies(url: String) {
CookieManager.getInstance().let {
val cookies = cookieJar.loadForRequest(url.toHttpUrl())
for (cookie in cookies) {
it.setCookie(url, cookie.toString())
}
}
}
companion object { companion object {
private const val VANISH_ACCOUNT_URL = "VanishAccountUrl" private const val VANISH_ACCOUNT_URL = "VanishAccountUrl"
private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl" private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl"

View file

@ -127,30 +127,64 @@ class CategoriesModel
/** /**
* Fetches details of every category associated with selected depictions, converts them into * Fetches details of every category associated with selected depictions, converts them into
* CategoryItem and returns them in a list. * CategoryItem and returns them in a list.
* If a selected depiction has no categories, the categories in which its P18 belongs are
* returned in the list.
* *
* @param selectedDepictions selected DepictItems * @param selectedDepictions selected DepictItems
* @return List of CategoryItem associated with selected depictions * @return List of CategoryItem associated with selected depictions
*/ */
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? = private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? {
Observable val observables = selectedDepictions.map { depictedItem ->
.fromIterable( if (depictedItem.commonsCategories.isEmpty()) {
selectedDepictions.map { it.commonsCategories }.flatten(), if (depictedItem.primaryImage == null) {
).map { categoryItem -> return@map Observable.just(emptyList<CategoryItem>())
categoryClient }
.getCategoriesByName( Observable.just(
categoryItem.name, depictedItem.primaryImage
categoryItem.name, ).map { image ->
SEARCH_CATS_LIMIT, categoryClient
).map { .getCategoriesOfImage(
CategoryItem( image,
it[0].name, SEARCH_CATS_LIMIT,
it[0].description, ).map {
it[0].thumbnail, it.map { category ->
it[0].isSelected, CategoryItem(
) category.name,
}.blockingGet() category.description,
}.toList() category.thumbnail,
.toObservable() category.isSelected,
)
}
}.blockingGet()
}.flatMapIterable { it }.toList()
.toObservable()
} else {
Observable
.fromIterable(
depictedItem.commonsCategories,
).map { categoryItem ->
categoryClient
.getCategoriesByName(
categoryItem.name,
categoryItem.name,
SEARCH_CATS_LIMIT,
).map {
CategoryItem(
it[0].name,
it[0].description,
it[0].thumbnail,
it[0].isSelected,
)
}.blockingGet()
}.toList()
.toObservable()
}
}
return Observable.concat(observables)
.scan(mutableListOf<CategoryItem>()) { accumulator, currentList ->
accumulator.apply { addAll(currentList) }
}
}
/** /**
* Fetches details of every category by their name, converts them into * Fetches details of every category by their name, converts them into

View file

@ -78,6 +78,24 @@ class CategoryClient
), ),
) )
/**
* Fetches categories belonging to an image (P18 of some wikidata entity).
*
* @param image P18 of some wikidata entity
* @param itemLimit How many categories to return
* @return Single Observable emitting the list of categories
*/
fun getCategoriesOfImage(
image: String,
itemLimit: Int,
): Single<List<CategoryItem>> =
responseMapper(
categoryInterface.getCategoriesByTitles(
"File:${image}",
itemLimit,
),
)
/** /**
* The method takes categoryName as input and returns a List of Subcategories * The method takes categoryName as input and returns a List of Subcategories
* It uses the generator query API to get the subcategories in a category, 500 at a time. * It uses the generator query API to get the subcategories in a category, 500 at a time.

View file

@ -61,6 +61,21 @@ interface CategoryInterface {
@Query("gacoffset") offset: Int, @Query("gacoffset") offset: Int,
): Single<MwQueryResponse> ): Single<MwQueryResponse>
/**
* Fetches non-hidden categories by titles.
*
* @param titles titles to fetch categories for (e.g. File:<P18 of a wikidata entity>)
* @param itemLimit How many categories to return
* @return MwQueryResponse
*/
@GET(
"w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70&gclshow=!hidden",
)
fun getCategoriesByTitles(
@Query("titles") titles: String?,
@Query("gcllimit") itemLimit: Int,
): Single<MwQueryResponse>
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50") @GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
fun getSubCategoryList( fun getSubCategoryList(
@Query("gcmtitle") categoryName: String, @Query("gcmtitle") categoryName: String,

View file

@ -101,7 +101,7 @@ data class Contribution constructor(
*/ */
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) = fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
uploadMediaDetails uploadMediaDetails
.associate { it.languageCode!! to it.captionText!! } .associate { it.languageCode!! to it.captionText }
.filter { it.value.isNotBlank() } .filter { it.value.isNotBlank() }
/** /**

View file

@ -24,6 +24,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.TreeMap import java.util.TreeMap
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -103,6 +104,18 @@ class ImageAdapter(
*/ */
private var imagePositionAsPerIncreasingOrder = 0 private var imagePositionAsPerIncreasingOrder = 0
/**
* Stores the number of images currently visible on the screen
*/
private val _currentImagesCount = MutableStateFlow(0)
val currentImagesCount = _currentImagesCount
/**
* Stores whether images are being loaded or not
*/
private val _isLoadingImages = MutableStateFlow(false)
val isLoadingImages = _isLoadingImages
/** /**
* Coroutine Dispatchers and Scope. * Coroutine Dispatchers and Scope.
*/ */
@ -184,8 +197,12 @@ class ImageAdapter(
// If the position is not already visited, that means the position is new then // If the position is not already visited, that means the position is new then
// finds the next actionable image position from all images // finds the next actionable image position from all images
if (!alreadyAddedPositions.contains(position)) { if (!alreadyAddedPositions.contains(position)) {
processThumbnailForActionedImage(holder, position, uploadingContributionList) processThumbnailForActionedImage(
holder,
position,
uploadingContributionList
)
_isLoadingImages.value = false
// If the position is already visited, that means the image is already present // If the position is already visited, that means the image is already present
// inside map, so it will fetch the image from the map and load in the holder // inside map, so it will fetch the image from the map and load in the holder
} else { } else {
@ -231,6 +248,7 @@ class ImageAdapter(
position: Int, position: Int,
uploadingContributionList: List<Contribution>, uploadingContributionList: List<Contribution>,
) { ) {
_isLoadingImages.value = true
val next = val next =
imageLoader.nextActionableImage( imageLoader.nextActionableImage(
allImages, allImages,
@ -252,6 +270,7 @@ class ImageAdapter(
actionableImagesMap[next] = allImages[next] actionableImagesMap[next] = allImages[next]
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder) alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
imagePositionAsPerIncreasingOrder++ imagePositionAsPerIncreasingOrder++
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
Glide Glide
.with(holder.image) .with(holder.image)
.load(allImages[next].uri) .load(allImages[next].uri)
@ -267,6 +286,7 @@ class ImageAdapter(
reachedEndOfFolder = true reachedEndOfFolder = true
notifyItemRemoved(position) notifyItemRemoved(position)
} }
_isLoadingImages.value = false
} }
/** /**
@ -372,6 +392,7 @@ class ImageAdapter(
emptyMap: TreeMap<Int, Image>, emptyMap: TreeMap<Int, Image>,
uploadedImages: List<Contribution> = ArrayList(), uploadedImages: List<Contribution> = ArrayList(),
) { ) {
_isLoadingImages.value = true
allImages = fixedImages allImages = fixedImages
val oldImageList: ArrayList<Image> = images val oldImageList: ArrayList<Image> = images
val newImageList: ArrayList<Image> = ArrayList(newImages) val newImageList: ArrayList<Image> = ArrayList(newImages)
@ -382,6 +403,7 @@ class ImageAdapter(
reachedEndOfFolder = false reachedEndOfFolder = false
selectedImages = ArrayList() selectedImages = ArrayList()
imagePositionAsPerIncreasingOrder = 0 imagePositionAsPerIncreasingOrder = 0
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
val diffResult = val diffResult =
DiffUtil.calculateDiff( DiffUtil.calculateDiff(
ImagesDiffCallback(oldImageList, newImageList), ImagesDiffCallback(oldImageList, newImageList),
@ -441,6 +463,7 @@ class ImageAdapter(
val entry = iterator.next() val entry = iterator.next()
if (entry.value == image) { if (entry.value == image) {
imagePositionAsPerIncreasingOrder -= 1 imagePositionAsPerIncreasingOrder -= 1
_currentImagesCount.value = imagePositionAsPerIncreasingOrder
iterator.remove() iterator.remove()
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1) alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
notifyItemRemoved(index) notifyItemRemoved(index)

View file

@ -12,8 +12,12 @@ import android.widget.ProgressBar
import android.widget.Switch import android.widget.Switch
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.contributions.Contribution import fr.free.nrw.commons.contributions.Contribution
@ -38,6 +42,10 @@ import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.FileProcessor import fr.free.nrw.commons.upload.FileProcessor
import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.upload.FileUtilsWrapper
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import java.util.TreeMap import java.util.TreeMap
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
@ -80,6 +88,12 @@ class ImageFragment :
*/ */
var allImages: ArrayList<Image> = ArrayList() var allImages: ArrayList<Image> = ArrayList()
/**
* Keeps track of switch state
*/
private val _switchState = MutableStateFlow(false)
val switchState = _switchState.asStateFlow()
/** /**
* View model Factory. * View model Factory.
*/ */
@ -214,7 +228,11 @@ class ImageFragment :
switch = binding?.switchWidget switch = binding?.switchWidget
switch?.visibility = View.VISIBLE switch?.visibility = View.VISIBLE
switch?.setOnCheckedChangeListener { _, isChecked -> onChangeSwitchState(isChecked) } _switchState.value = switch?.isChecked ?: false
switch?.setOnCheckedChangeListener { _, isChecked ->
onChangeSwitchState(isChecked)
_switchState.value = isChecked
}
selectorRV = binding?.selectorRv selectorRV = binding?.selectorRv
loader = binding?.loader loader = binding?.loader
progressLayout = binding?.progressLayout progressLayout = binding?.progressLayout
@ -234,6 +252,28 @@ class ImageFragment :
return binding?.root return binding?.root
} }
/**
* onViewCreated
* Updates empty text view visibility based on image count, switch state, and loading status.
*/
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
combine(
imageAdapter.currentImagesCount,
switchState,
imageAdapter.isLoadingImages
) { imageCount, isChecked, isLoadingImages ->
Triple(imageCount, isChecked, isLoadingImages)
}.collect { (imageCount, isChecked, isLoadingImages) ->
binding?.allImagesUploadedOrMarked?.isVisible =
!isLoadingImages && !isChecked && imageCount == 0 && (switch?.isVisible == true)
}
}
}
}
private fun onChangeSwitchState(checked: Boolean) { private fun onChangeSwitchState(checked: Boolean) {
if (checked) { if (checked) {
showAlreadyActionedImages = true showAlreadyActionedImages = true

View file

@ -267,11 +267,11 @@ class DescriptionEditActivity :
applicationContext, applicationContext,
media, media,
mediaDetail.languageCode!!, mediaDetail.languageCode!!,
mediaDetail.captionText!!, mediaDetail.captionText,
).subscribeOn(Schedulers.io()) ).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { s: Boolean? -> .subscribe { s: Boolean? ->
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText!! updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
media.captions = updatedCaptions media.captions = updatedCaptions
Timber.d("Caption is added.") Timber.d("Caption is added.")
}, },

View file

@ -6,6 +6,7 @@ import dagger.android.AndroidInjectionModule
import dagger.android.AndroidInjector import dagger.android.AndroidInjector
import dagger.android.support.AndroidSupportInjectionModule import dagger.android.support.AndroidSupportInjectionModule
import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.activity.SingleWebViewActivity
import fr.free.nrw.commons.auth.LoginActivity import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.contributions.ContributionsModule import fr.free.nrw.commons.contributions.ContributionsModule
import fr.free.nrw.commons.explore.SearchModule import fr.free.nrw.commons.explore.SearchModule
@ -51,6 +52,8 @@ interface CommonsApplicationComponent : AndroidInjector<ApplicationlessInjection
fun inject(activity: LoginActivity) fun inject(activity: LoginActivity)
fun inject(activity: SingleWebViewActivity)
fun inject(fragment: SettingsFragment) fun inject(fragment: SettingsFragment)
fun inject(fragment: MoreBottomSheetFragment) fun inject(fragment: MoreBottomSheetFragment)

View file

@ -1586,7 +1586,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
mediaDetail: UploadMediaDetail, mediaDetail: UploadMediaDetail,
updatedCaptions: MutableMap<String, String> updatedCaptions: MutableMap<String, String>
) { ) {
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText!! updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
media!!.captions = updatedCaptions media!!.captions = updatedCaptions
} }
@ -1754,10 +1754,11 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
return return
} }
ProfileActivity.startYourself( ProfileActivity.startYourself(
activity, requireActivity(), // Ensure this is a non-null Activity context
media!!.user, media?.user ?: "", // Provide a fallback value if media?.user is null
sessionManager.userName != media!!.user sessionManager.userName != media?.user // This can remain as is, null check will apply
) )
} }
/** /**

View file

@ -1,265 +0,0 @@
package fr.free.nrw.commons.profile;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.FileProvider;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.contributions.ContributionsFragment;
import fr.free.nrw.commons.databinding.ActivityProfileBinding;
import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.DialogUtil;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
/**
* This activity will set two tabs, achievements and
* each tab will have their own fragments
*/
public class ProfileActivity extends BaseActivity {
private FragmentManager supportFragmentManager;
public ActivityProfileBinding binding;
@Inject
SessionManager sessionManager;
private ViewPagerAdapter viewPagerAdapter;
private AchievementsFragment achievementsFragment;
private LeaderboardFragment leaderboardFragment;
public static final String KEY_USERNAME ="username";
public static final String KEY_SHOULD_SHOW_CONTRIBUTIONS ="shouldShowContributions";
String userName;
private boolean shouldShowContributions;
ContributionsFragment contributionsFragment;
public void setScroll(boolean canScroll){
binding.viewPager.setCanScroll(canScroll);
}
@Override
protected void onRestoreInstanceState(final Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState != null) {
userName = savedInstanceState.getString(KEY_USERNAME);
shouldShowContributions = savedInstanceState.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityProfileBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setSupportActionBar(binding.toolbarBinding.toolbar);
binding.toolbarBinding.toolbar.setNavigationOnClickListener(view -> {
onSupportNavigateUp();
});
userName = getIntent().getStringExtra(KEY_USERNAME);
setTitle(userName);
shouldShowContributions = getIntent().getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false);
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.tabLayout.setupWithViewPager(binding.viewPager);
setTabs();
}
/**
* Navigate up event
* @return boolean
*/
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
/**
* Creates a way to change current activity to AchievementActivity
*
* @param context
*/
public static void startYourself(final Context context, final String userName,
final boolean shouldShowContributions) {
Intent intent = new Intent(context, ProfileActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(KEY_USERNAME, userName);
intent.putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions);
context.startActivity(intent);
}
/**
* Set the tabs for the fragments
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
achievementsFragment = new AchievementsFragment();
Bundle achievementsBundle = new Bundle();
achievementsBundle.putString(KEY_USERNAME, userName);
achievementsFragment.setArguments(achievementsBundle);
fragmentList.add(achievementsFragment);
titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase());
leaderboardFragment = new LeaderboardFragment();
Bundle leaderBoardBundle = new Bundle();
leaderBoardBundle.putString(KEY_USERNAME, userName);
leaderboardFragment.setArguments(leaderBoardBundle);
fragmentList.add(leaderboardFragment);
titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT));
contributionsFragment = new ContributionsFragment();
Bundle contributionsListBundle = new Bundle();
contributionsListBundle.putString(KEY_USERNAME, userName);
contributionsFragment.setArguments(contributionsListBundle);
fragmentList.add(contributionsFragment);
titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT));
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
}
@Override
public void onDestroy() {
super.onDestroy();
getCompositeDisposable().clear();
}
/**
* To inflate menu
* @param menu Menu
* @return boolean
*/
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
final MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.menu_about, menu);
return super.onCreateOptionsMenu(menu);
}
/**
* To receive the id of selected item and handle further logic for that selected item
* @param item MenuItem
* @return boolean
*/
@Override
public boolean onOptionsItemSelected(final MenuItem item) {
// take screenshot in form of bitmap and show it in Alert Dialog
if (item.getItemId() == R.id.share_app_icon) {
final View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
final Bitmap screenShot = Utils.getScreenShot(rootView);
showAlert(screenShot);
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* It displays the alertDialog with Image of screenshot
* @param screenshot screenshot of the present screen
*/
public void showAlert(final Bitmap screenshot) {
final LayoutInflater factory = LayoutInflater.from(this);
final View view = factory.inflate(R.layout.image_alert_layout, null);
final ImageView screenShotImage = view.findViewById(R.id.alert_image);
screenShotImage.setImageBitmap(screenshot);
final TextView shareMessage = view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message);
DialogUtil.showAlertDialog(this,
null,
null,
getString(R.string.about_translate_proceed),
getString(R.string.cancel),
() -> shareScreen(screenshot),
() -> {},
view
);
}
/**
* To take bitmap and store it temporary storage and share it
* @param bitmap bitmap of screenshot
*/
void shareScreen(final Bitmap bitmap) {
try {
final File file = new File(getExternalCacheDir(), "screen.png");
final FileOutputStream fileOutputStream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
fileOutputStream.flush();
fileOutputStream.close();
file.setReadable(true, false);
final Uri fileUri = FileProvider
.getUriForFile(getApplicationContext(),
getPackageName() + ".provider", file);
grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
intent.setType("image/png");
startActivity(Intent.createChooser(intent, getString(R.string.share_image_via)));
} catch (final IOException e) {
e.printStackTrace();
}
}
@Override
protected void onSaveInstanceState(@NonNull final Bundle outState) {
outState.putString(KEY_USERNAME, userName);
outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions);
super.onSaveInstanceState(outState);
}
@Override
public void onBackPressed() {
// Checking if MediaDetailPagerFragment is visible, If visible then show ContributionListFragment else close the ProfileActivity
if(contributionsFragment != null && contributionsFragment.getMediaDetailPagerFragment() != null && contributionsFragment.getMediaDetailPagerFragment().isVisible()) {
contributionsFragment.backButtonClicked();
binding.tabLayout.setVisibility(View.VISIBLE);
}else {
super.onBackPressed();
}
}
/**
* To set the visibility of tab layout
* @param isVisible boolean
*/
public void setTabLayoutVisibility(boolean isVisible) {
binding.tabLayout.setVisibility(isVisible ? View.VISIBLE : View.GONE);
}
}

View file

@ -0,0 +1,229 @@
package fr.free.nrw.commons.profile
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.util.Log
import android.view.*
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.contributions.ContributionsFragment
import fr.free.nrw.commons.databinding.ActivityProfileBinding
import fr.free.nrw.commons.profile.achievements.AchievementsFragment
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.DialogUtil
import java.io.File
import java.io.FileOutputStream
import java.util.*
import javax.inject.Inject
/**
* This activity will set two tabs, achievements and
* each tab will have their own fragments
*/
class ProfileActivity : BaseActivity() {
lateinit var binding: ActivityProfileBinding
@Inject
lateinit var sessionManager: SessionManager
private lateinit var viewPagerAdapter: ViewPagerAdapter
private lateinit var achievementsFragment: AchievementsFragment
private lateinit var leaderboardFragment: LeaderboardFragment
private lateinit var userName: String
private var shouldShowContributions: Boolean = false
private var contributionsFragment: ContributionsFragment? = null
fun setScroll(canScroll: Boolean) {
binding.viewPager.setCanScroll(canScroll)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
savedInstanceState.let {
userName = it.getString(KEY_USERNAME, "")
shouldShowContributions = it.getBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.toolbarBinding.toolbar)
binding.toolbarBinding.toolbar.setNavigationOnClickListener {
onSupportNavigateUp()
}
userName = intent.getStringExtra(KEY_USERNAME) ?: ""
title = userName
shouldShowContributions = intent.getBooleanExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, false)
viewPagerAdapter = ViewPagerAdapter(supportFragmentManager)
binding.viewPager.adapter = viewPagerAdapter
binding.tabLayout.setupWithViewPager(binding.viewPager)
setTabs()
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
private fun setTabs() {
val fragmentList = mutableListOf<Fragment>()
val titleList = mutableListOf<String>()
// Add Achievements tab
achievementsFragment = AchievementsFragment().apply {
arguments = Bundle().apply {
putString(KEY_USERNAME, userName)
}
}
fragmentList.add(achievementsFragment)
titleList.add(resources.getString(R.string.achievements_tab_title).uppercase())
// Add Leaderboard tab
leaderboardFragment = LeaderboardFragment().apply {
arguments = Bundle().apply {
putString(KEY_USERNAME, userName)
}
}
fragmentList.add(leaderboardFragment)
titleList.add(resources.getString(R.string.leaderboard_tab_title).uppercase(Locale.ROOT))
// Add Contributions tab
contributionsFragment = ContributionsFragment().apply {
arguments = Bundle().apply {
putString(KEY_USERNAME, userName)
}
}
contributionsFragment?.let {
fragmentList.add(it)
titleList.add(getString(R.string.contributions_fragment).uppercase(Locale.ROOT))
}
viewPagerAdapter.setTabData(fragmentList, titleList)
viewPagerAdapter.notifyDataSetChanged()
}
public override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_about, menu)
return super.onCreateOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.share_app_icon -> {
val rootView = window.decorView.findViewById<View>(android.R.id.content)
val screenShot = Utils.getScreenShot(rootView)
if (screenShot == null) {
Log.e("ERROR", "ScreenShot is null")
return false
}
showAlert(screenShot)
true
}
else -> super.onOptionsItemSelected(item)
}
}
fun showAlert(screenshot: Bitmap) {
val view = layoutInflater.inflate(R.layout.image_alert_layout, null)
val screenShotImage = view.findViewById<ImageView>(R.id.alert_image)
val shareMessage = view.findViewById<TextView>(R.id.alert_text)
screenShotImage.setImageBitmap(screenshot)
shareMessage.setText(R.string.achievements_share_message)
DialogUtil.showAlertDialog(
this,
null,
null,
getString(R.string.about_translate_proceed),
getString(R.string.cancel),
{ shareScreen(screenshot) },
{},
view
)
}
private fun shareScreen(bitmap: Bitmap) {
try {
val file = File(externalCacheDir, "screen.png")
FileOutputStream(file).use { out ->
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
out.flush()
}
file.setReadable(true, false)
val fileUri = FileProvider.getUriForFile(
applicationContext,
"$packageName.provider",
file
)
grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
val intent = Intent(Intent.ACTION_SEND).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra(Intent.EXTRA_STREAM, fileUri)
type = "image/png"
}
startActivity(Intent.createChooser(intent, getString(R.string.share_image_via)))
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString(KEY_USERNAME, userName)
outState.putBoolean(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions)
}
override fun onBackPressed() {
if (contributionsFragment?.mediaDetailPagerFragment?.isVisible == true) {
contributionsFragment?.backButtonClicked()
binding.tabLayout.visibility = View.VISIBLE
} else {
super.onBackPressed()
}
}
fun setTabLayoutVisibility(isVisible: Boolean) {
binding.tabLayout.visibility = if (isVisible) View.VISIBLE else View.GONE
}
companion object {
const val KEY_USERNAME = "username"
const val KEY_SHOULD_SHOW_CONTRIBUTIONS = "shouldShowContributions"
@JvmStatic
fun startYourself(context: Context, userName: String, shouldShowContributions: Boolean) {
val intent = Intent(context, ProfileActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP)
putExtra(KEY_USERNAME, userName)
putExtra(KEY_SHOULD_SHOW_CONTRIBUTIONS, shouldShowContributions)
}
context.startActivity(intent)
}
}
}

View file

@ -58,7 +58,7 @@ class LeaderboardListAdapter : PagedListAdapter<LeaderboardList, ListViewHolder>
if (view.context is ProfileActivity) { if (view.context is ProfileActivity) {
((view.context) as Activity).finish() ((view.context) as Activity).finish()
} }
ProfileActivity.startYourself(view.context, item.username, true) ProfileActivity.startYourself(view.context, item.username?:"", true)
} }
} }
} }

View file

@ -14,11 +14,15 @@ class QuizController {
private val quiz: ArrayList<QuizQuestion> = ArrayList() private val quiz: ArrayList<QuizQuestion> = ArrayList()
private val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg" companion object{
private val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"
private val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg" const val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg"
private val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png" const val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"
private val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg" const val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg"
const val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png"
const val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg"
}
fun initialize(context: Context) { fun initialize(context: Context) {
val q1 = QuizQuestion( val q1 = QuizQuestion(

View file

@ -45,12 +45,12 @@ class ReviewActivity : BaseActivity() {
private var hasNonHiddenCategories = false private var hasNonHiddenCategories = false
var media: Media? = null var media: Media? = null
private val SAVED_MEDIA = "saved_media" private val savedMedia = "saved_media"
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
media?.let { media?.let {
outState.putParcelable(SAVED_MEDIA, it) outState.putParcelable(savedMedia, it)
} }
} }
@ -90,8 +90,8 @@ class ReviewActivity : BaseActivity() {
PorterDuff.Mode.SRC_IN PorterDuff.Mode.SRC_IN
) )
if (savedInstanceState?.getParcelable<Media>(SAVED_MEDIA) != null) { if (savedInstanceState?.getParcelable<Media>(savedMedia) != null) {
updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!) updateImage(savedInstanceState.getParcelable(savedMedia)!!)
setUpMediaDetailOnOrientation() setUpMediaDetailOnOrientation()
} else { } else {
runRandomizer() runRandomizer()

View file

@ -31,7 +31,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
lateinit var sessionManager: SessionManager lateinit var sessionManager: SessionManager
// Constant variable used to store user's key name for onSaveInstanceState method // Constant variable used to store user's key name for onSaveInstanceState method
private val SAVED_USER = "saved_user" private val savedUser = "saved_user"
// Variable that stores the value of user // Variable that stores the value of user
private var user: String? = null private var user: String? = null
@ -129,7 +129,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
question = getString(R.string.review_thanks) question = getString(R.string.review_thanks)
user = reviewActivity.reviewController.firstRevision?.user() user = reviewActivity.reviewController.firstRevision?.user()
?: savedInstanceState?.getString(SAVED_USER) ?: savedInstanceState?.getString(savedUser)
//if the user is null because of whatsoever reason, review will not be sent anyways //if the user is null because of whatsoever reason, review will not be sent anyways
if (!user.isNullOrEmpty()) { if (!user.isNullOrEmpty()) {
@ -172,7 +172,7 @@ class ReviewImageFragment : CommonsDaggerSupportFragment() {
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
//Save user name when configuration changes happen //Save user name when configuration changes happen
outState.putString(SAVED_USER, user) outState.putString(savedUser, user)
} }
private val reviewCallback: ReviewController.ReviewCallback private val reviewCallback: ReviewController.ReviewCallback

View file

@ -33,6 +33,7 @@ import com.karumi.dexter.MultiplePermissionsReport
import com.karumi.dexter.PermissionToken import com.karumi.dexter.PermissionToken
import com.karumi.dexter.listener.PermissionRequest import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import fr.free.nrw.commons.BuildConfig.MOBILE_META_URL
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.activity.SingleWebViewActivity import fr.free.nrw.commons.activity.SingleWebViewActivity
@ -85,7 +86,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var languageHistoryListView: ListView? = null private var languageHistoryListView: ListView? = null
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>> private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"
private val cameraPickLauncherForResult: ActivityResultLauncher<Intent> = private val cameraPickLauncherForResult: ActivityResultLauncher<Intent> =
registerForActivityResult(StartActivityForResult()) { result -> registerForActivityResult(StartActivityForResult()) { result ->
@ -271,6 +271,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
findPreference<Preference>("managed_exif_tags")?.isEnabled = false findPreference<Preference>("managed_exif_tags")?.isEnabled = false
findPreference<Preference>("openDocumentPhotoPickerPref")?.isEnabled = false findPreference<Preference>("openDocumentPhotoPickerPref")?.isEnabled = false
findPreference<Preference>("inAppCameraLocationPref")?.isEnabled = false findPreference<Preference>("inAppCameraLocationPref")?.isEnabled = false
findPreference<Preference>("vanishAccount")?.isEnabled = false
} }
} }
@ -511,6 +512,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
@Suppress("LongLine") @Suppress("LongLine")
companion object { companion object {
const val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"
private const val VANISH_ACCOUNT_URL = "https://meta.m.wikimedia.org/wiki/Special:Contact/accountvanishapps" private const val VANISH_ACCOUNT_URL = "https://meta.m.wikimedia.org/wiki/Special:Contact/accountvanishapps"
private const val VANISH_ACCOUNT_SUCCESS_URL = "https://meta.m.wikimedia.org/wiki/Special:GlobalVanishRequest/vanished" private const val VANISH_ACCOUNT_SUCCESS_URL = "https://meta.m.wikimedia.org/wiki/Special:GlobalVanishRequest/vanished"
/** /**

View file

@ -10,7 +10,6 @@ import fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP
import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK import fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK
import fr.free.nrw.commons.utils.ImageUtilsWrapper import fr.free.nrw.commons.utils.ImageUtilsWrapper
import io.reactivex.Single import io.reactivex.Single
import io.reactivex.functions.Function
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import timber.log.Timber import timber.log.Timber
@ -26,7 +25,7 @@ class ImageProcessingService @Inject constructor(
private val fileUtilsWrapper: FileUtilsWrapper, private val fileUtilsWrapper: FileUtilsWrapper,
private val imageUtilsWrapper: ImageUtilsWrapper, private val imageUtilsWrapper: ImageUtilsWrapper,
private val readFBMD: ReadFBMD, private val readFBMD: ReadFBMD,
private val EXIFReader: EXIFReader, private val exifReader: EXIFReader,
private val mediaClient: MediaClient private val mediaClient: MediaClient
) { ) {
/** /**
@ -94,7 +93,7 @@ class ImageProcessingService @Inject constructor(
* the presence of some basic Exif metadata. * the presence of some basic Exif metadata.
*/ */
private fun checkEXIF(filepath: String): Single<Int> = private fun checkEXIF(filepath: String): Single<Int> =
EXIFReader.processMetadata(filepath) exifReader.processMetadata(filepath)
/** /**

View file

@ -706,17 +706,64 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
private fun receiveInternalSharedItems() { private fun receiveInternalSharedItems() {
val intent = intent val intent = intent
Timber.d("Intent has EXTRA_FILES: ${EXTRA_FILES}")
uploadableFiles = try {
// Check if intent has the extra before trying to read it
if (!intent.hasExtra(EXTRA_FILES)) {
Timber.w("No EXTRA_FILES found in intent")
mutableListOf()
} else {
// Try to get the files as Parcelable array
val files = if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
intent.getParcelableArrayListExtra(EXTRA_FILES, UploadableFile::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableArrayListExtra<UploadableFile>(EXTRA_FILES)
}
Timber.d("Received intent %s with action %s", intent.toString(), intent.action) // Convert to mutable list or return empty list if null
files?.toMutableList() ?: run {
uploadableFiles = mutableListOf<UploadableFile>().apply { Timber.w("Files array was null")
addAll(intent.getParcelableArrayListExtra(EXTRA_FILES) ?: emptyList()) mutableListOf()
}
}
} catch (e: Exception) {
Timber.e(e, "Error reading files from intent")
mutableListOf()
}
// Log the result for debugging
isMultipleFilesSelected = uploadableFiles.size > 1
Timber.i("Received files count: ${uploadableFiles.size}")
uploadableFiles.forEachIndexed { index, file ->
Timber.d("File $index path: ${file.getFilePath()}")
}
// Handle other extras with null safety
place = try {
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(PLACE_OBJECT, Place::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(PLACE_OBJECT)
}
} catch (e: Exception) {
Timber.e(e, "Error reading place")
null
}
prevLocation = try {
if (VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE, LatLng::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE)
}
} catch (e: Exception) {
Timber.e(e, "Error reading location")
null
} }
isMultipleFilesSelected = uploadableFiles.size > 1
Timber.i("Received multiple upload %s", uploadableFiles.size)
place = intent.getParcelableExtra(PLACE_OBJECT)
prevLocation = intent.getParcelableExtra(LOCATION_BEFORE_IMAGE_CAPTURE)
isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false) isInAppCameraUpload = intent.getBooleanExtra(IN_APP_CAMERA_UPLOAD, false)
resetDirectPrefs() resetDirectPrefs()
} }
@ -826,6 +873,21 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
onBackPressedCallback.remove() onBackPressedCallback.remove()
} }
/**
* Overrides the back button to make sure the user is prepared to lose their progress
*/
@SuppressLint("MissingSuperCall")
override fun onBackPressed() {
showAlertDialog(
this,
getString(R.string.back_button_warning),
getString(R.string.back_button_warning_desc),
getString(R.string.back_button_continue),
getString(R.string.back_button_warning),
null
) { finish() }
}
/** /**
* If the user uploads more than 1 file informs that * If the user uploads more than 1 file informs that
* depictions/categories apply to all pictures of a multi upload. * depictions/categories apply to all pictures of a multi upload.
@ -933,7 +995,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C
companion object { companion object {
private var uploadIsOfAPlace = false private var uploadIsOfAPlace = false
const val EXTRA_FILES: String = "commons_image_exta" const val EXTRA_FILES: String = "commons_image_extra"
const val LOCATION_BEFORE_IMAGE_CAPTURE: String = "user_location_before_image_capture" const val LOCATION_BEFORE_IMAGE_CAPTURE: String = "user_location_before_image_capture"
const val IN_APP_CAMERA_UPLOAD: String = "in_app_camera_upload" const val IN_APP_CAMERA_UPLOAD: String = "in_app_camera_upload"

View file

@ -23,14 +23,14 @@ data class UploadMediaDetail(
* The caption text for the item being uploaded. * The caption text for the item being uploaded.
* @param captionText The caption text. * @param captionText The caption text.
*/ */
var captionText: String? = "", var captionText: String = "",
) : Parcelable { ) : Parcelable {
fun javaCopy() = copy() fun javaCopy() = copy()
constructor(place: Place?) : this( constructor(place: Place?) : this(
place?.language, place?.language,
place?.longDescription, place?.longDescription,
place?.name, place?.name ?: "",
) )
/** /**

View file

@ -140,7 +140,7 @@ class CategoriesPresenter
*/ */
private fun getImageTitleList(): List<String> = private fun getImageTitleList(): List<String> =
repository.getUploads() repository.getUploads()
.map { it.uploadMediaDetails[0].captionText!! } .map { it.uploadMediaDetails[0].captionText }
.filterNot { TextUtils.isEmpty(it) } .filterNot { TextUtils.isEmpty(it) }
/** /**

View file

@ -68,6 +68,9 @@ data class DepictedItem constructor(
entity.id(), entity.id(),
) )
val primaryImage: String?
get() = imageUrl?.split('-')?.lastOrNull()
override fun equals(other: Any?) = override fun equals(other: Any?) =
when { when {
this === other -> true this === other -> true

View file

@ -16,6 +16,9 @@ import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import fr.free.nrw.commons.R import fr.free.nrw.commons.R
import fr.free.nrw.commons.upload.UploadActivity import fr.free.nrw.commons.upload.UploadActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object PermissionUtils { object PermissionUtils {
@ -130,7 +133,7 @@ object PermissionUtils {
vararg permissions: String vararg permissions: String
) { ) {
if (hasPartialAccess(activity)) { if (hasPartialAccess(activity)) {
Thread(onPermissionGranted).start() CoroutineScope(Dispatchers.Main).launch { onPermissionGranted.run() }
return return
} }
checkPermissionsAndPerformAction( checkPermissionsAndPerformAction(
@ -166,13 +169,15 @@ object PermissionUtils {
rationaleMessage: Int, rationaleMessage: Int,
vararg permissions: String vararg permissions: String
) { ) {
val scope = CoroutineScope(Dispatchers.Main)
Dexter.withActivity(activity) Dexter.withActivity(activity)
.withPermissions(*permissions) .withPermissions(*permissions)
.withListener(object : MultiplePermissionsListener { .withListener(object : MultiplePermissionsListener {
override fun onPermissionsChecked(report: MultiplePermissionsReport) { override fun onPermissionsChecked(report: MultiplePermissionsReport) {
when { when {
report.areAllPermissionsGranted() || hasPartialAccess(activity) -> report.areAllPermissionsGranted() || hasPartialAccess(activity) ->
Thread(onPermissionGranted).start() scope.launch { onPermissionGranted.run() }
report.isAnyPermissionPermanentlyDenied -> { report.isAnyPermissionPermanentlyDenied -> {
DialogUtil.showAlertDialog( DialogUtil.showAlertDialog(
activity, activity,
@ -189,7 +194,7 @@ object PermissionUtils {
null, null null, null
) )
} }
else -> Thread(onPermissionDenied).start() else -> scope.launch { onPermissionDenied?.run() }
} }
} }

View file

@ -21,9 +21,14 @@ abstract class SwipableCardView @JvmOverloads constructor(
defStyleAttr: Int = 0 defStyleAttr: Int = 0
) : CardView(context, attrs, defStyleAttr) { ) : CardView(context, attrs, defStyleAttr) {
companion object{
const val MINIMUM_THRESHOLD_FOR_SWIPE = 100f
}
private var x1 = 0f private var x1 = 0f
private var x2 = 0f private var x2 = 0f
private val MINIMUM_THRESHOLD_FOR_SWIPE = 100f
init { init {
interceptOnTouchListener() interceptOnTouchListener()

View file

@ -8,7 +8,7 @@
android:fillViewport="true" android:fillViewport="true"
tools:ignore="ContentDescription" > tools:ignore="ContentDescription" >
<!-- TODO Add ContentDescription For ALL Images Added ignore to suppress Lints --> <!-- TODO Add ContentDescription For ALL Images Added ignore to suppress Lints -->
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/layout" android:id="@+id/layout"

View file

@ -49,6 +49,20 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/all_images_uploaded_or_marked"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:textSize="16sp"
android:padding="@dimen/standard_gap"
android:textColor="@color/text_color_selector"
android:text="@string/congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
/>
<ProgressBar <ProgressBar
android:id="@+id/loader" android:id="@+id/loader"

View file

@ -875,6 +875,11 @@
<string name="usages_on_commons_heading">كومنز</string> <string name="usages_on_commons_heading">كومنز</string>
<string name="usages_on_other_wikis_heading">مواقع ويكي أخرى</string> <string name="usages_on_other_wikis_heading">مواقع ويكي أخرى</string>
<string name="file_usages_container_heading">حالات استخدام الملف</string> <string name="file_usages_container_heading">حالات استخدام الملف</string>
<string name="title_activity_single_web_view">نشاط عرض ويب واحد</string>
<string name="account">حساب</string>
<string name="vanish_account">حذف الحساب</string>
<string name="account_vanish_request_confirm_title">تحذير من اختفاء الحساب</string>
<string name="account_vanish_request_confirm">الاختفاء هو &lt;b&gt;الملاذ الأخير&lt;/b&gt; ويجب &lt;b&gt;استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد&lt;/b&gt; وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.&lt;br/&gt;&lt;br/&gt; يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. &lt;b&gt;لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع&lt;/b&gt; .</string>
<string name="caption">الشرح</string> <string name="caption">الشرح</string>
<string name="caption_copied_to_clipboard">تم نسخ التسمية التوضيحية إلى الحافظة</string> <string name="caption_copied_to_clipboard">تم نسخ التسمية التوضيحية إلى الحافظة</string>
</resources> </resources>

View file

@ -708,6 +708,7 @@
<string name="usages_on_other_wikis_heading">다른 위키</string> <string name="usages_on_other_wikis_heading">다른 위키</string>
<string name="file_usages_container_heading">이 파일을 사용하는 문서</string> <string name="file_usages_container_heading">이 파일을 사용하는 문서</string>
<string name="account">계정</string> <string name="account">계정</string>
<string name="vanish_account">계정 버리기</string>
<string name="caption">캡션</string> <string name="caption">캡션</string>
<string name="caption_copied_to_clipboard">캡션이 클립보드에 복사되었습니다</string> <string name="caption_copied_to_clipboard">캡션이 클립보드에 복사되었습니다</string>
</resources> </resources>

View file

@ -109,7 +109,7 @@
<string name="welcome_copyright_text">Сиздин сүрөттөр дүйнө жүзүндөгү адамдардын билим алышына өбөлгө түзүүдө.</string> <string name="welcome_copyright_text">Сиздин сүрөттөр дүйнө жүзүндөгү адамдардын билим алышына өбөлгө түзүүдө.</string>
<string name="welcome_copyright_subtext">Интернетте жарыяланган автордук укукка ээ сүрөттөрдөн, ошондой эле плакаттардан жана китептердин мукабасынан ж.б. четтеңиз.</string> <string name="welcome_copyright_subtext">Интернетте жарыяланган автордук укукка ээ сүрөттөрдөн, ошондой эле плакаттардан жана китептердин мукабасынан ж.б. четтеңиз.</string>
<string name="welcome_final_text">Сизге бул түшүнүктүүбү?</string> <string name="welcome_final_text">Сизге бул түшүнүктүүбү?</string>
<string name="welcome_final_button_text">Ооба !</string> <string name="welcome_final_button_text">Ооба!</string>
<string name="detail_panel_cats_label">Категориялар</string> <string name="detail_panel_cats_label">Категориялар</string>
<string name="detail_panel_cats_loading">Жүктөлүүдө…</string> <string name="detail_panel_cats_loading">Жүктөлүүдө…</string>
<string name="detail_panel_cats_none">Тандалган жок</string> <string name="detail_panel_cats_none">Тандалган жок</string>

View file

@ -831,4 +831,8 @@
<string name="usages_on_commons_heading">Commons</string> <string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Andere wikis</string> <string name="usages_on_other_wikis_heading">Andere wikis</string>
<string name="file_usages_container_heading">Bestandsgebruik</string> <string name="file_usages_container_heading">Bestandsgebruik</string>
<string name="title_activity_single_web_view">Activiteit enkele webraadpleging</string>
<string name="account">Account</string>
<string name="vanish_account">Account laten verdwijnen</string>
<string name="account_vanish_request_confirm_title">Waarschuwing verwijdering account</string>
</resources> </resources>

View file

@ -813,7 +813,11 @@
<string name="usages_on_commons_heading">Commons</string> <string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Andra wikier</string> <string name="usages_on_other_wikis_heading">Andra wikier</string>
<string name="file_usages_container_heading">Filanvändning</string> <string name="file_usages_container_heading">Filanvändning</string>
<string name="title_activity_single_web_view">SingleWebViewActivity</string>
<string name="account">Konto</string> <string name="account">Konto</string>
<string name="vanish_account">Få kontot att försvinna</string>
<string name="account_vanish_request_confirm_title">Varning om försvinnande konto</string>
<string name="account_vanish_request_confirm">Att få kontot att försvinna är en &lt;b&gt;sista utväg&lt;/b&gt; och bör &lt;b&gt;endast användas när du vill sluta redigera för alltid&lt;/b&gt; och även dölja så många av dina tidigare associationer som möjligt.&lt;br/&gt;&lt;br/&gt;Konton raderas på Wikimedia Commons genom att ändra kontonamnet för att göra så att andra inte kan känna igen bidragen i en process som kallas kontoförsvinnande. &lt;b&gt;Försvinnande garanterar inte fullständig anonymitet eller att bidrag tas bort från projekten&lt;/b&gt;.</string>
<string name="caption">Bildtext</string> <string name="caption">Bildtext</string>
<string name="caption_copied_to_clipboard">Bildtext kopierades till urklipp</string> <string name="caption_copied_to_clipboard">Bildtext kopierades till urklipp</string>
</resources> </resources>

View file

@ -867,5 +867,6 @@ Upload your first media by tapping on the add button.</string>
<string name="account_vanish_request_confirm"><![CDATA[Vanishing is a <b>last resort</b> and should <b>only be used when you wish to stop editing forever</b> and also to hide as many of your past associations as possible.<br/><br/>Account deletion on Wikimedia Commons is done by changing your account name to make it so others cannot recognize your contributions in a process called account vanishing. <b>Vanishing does not guarantee complete anonymity or remove contributions to the projects</b>.]]></string> <string name="account_vanish_request_confirm"><![CDATA[Vanishing is a <b>last resort</b> and should <b>only be used when you wish to stop editing forever</b> and also to hide as many of your past associations as possible.<br/><br/>Account deletion on Wikimedia Commons is done by changing your account name to make it so others cannot recognize your contributions in a process called account vanishing. <b>Vanishing does not guarantee complete anonymity or remove contributions to the projects</b>.]]></string>
<string name="caption">Caption</string> <string name="caption">Caption</string>
<string name="caption_copied_to_clipboard">Caption copied to clipboard</string> <string name="caption_copied_to_clipboard">Caption copied to clipboard</string>
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">Congratulations, all pictures in this album have been either uploaded or marked as not for upload.</string>
</resources> </resources>

View file

@ -20,6 +20,7 @@
<item name="reviewHeading">@color/white</item> <item name="reviewHeading">@color/white</item>
<item name="aboutIconsColor">@color/white</item> <item name="aboutIconsColor">@color/white</item>
<item name="caption_description_text_color">@color/white</item> <item name="caption_description_text_color">@color/white</item>
<item name="android:statusBarColor">@color/main_background_dark</item>
<item name="semitransparentText">@color/commons_app_blue_dark</item> <item name="semitransparentText">@color/commons_app_blue_dark</item>
<item name="subBackground">@color/sub_background_dark</item> <item name="subBackground">@color/sub_background_dark</item>

View file

@ -1,7 +1,9 @@
package fr.free.nrw.commons.category package fr.free.nrw.commons.category
import categoryItem import categoryItem
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.times
import com.nhaarman.mockitokotlin2.verify import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever import com.nhaarman.mockitokotlin2.whenever
import depictedItem import depictedItem
@ -90,14 +92,18 @@ class CategoriesModelTest {
val depictedItem = val depictedItem =
depictedItem( depictedItem(
commonsCategories = commonsCategories =
listOf( listOf(
CategoryItem( CategoryItem(
"depictionCategory", "depictionCategory",
"", "",
"", "",
false, false,
),
), ),
),
)
val depictedItemWithoutCategories =
depictedItem(
imageUrl = "testUrl"
) )
whenever(gpsCategoryModel.categoriesFromLocation) whenever(gpsCategoryModel.categoriesFromLocation)
@ -159,6 +165,23 @@ class CategoriesModelTest {
), ),
), ),
) )
whenever(
categoryClient.getCategoriesOfImage(
"testUrl",
25,
),
).thenReturn(
Single.just(
listOf(
CategoryItem(
"categoriesOfP18",
"",
"",
false,
),
),
),
)
val imageTitleList = listOf("Test") val imageTitleList = listOf("Test")
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel) CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
.searchAll("", imageTitleList, listOf(depictedItem)) .searchAll("", imageTitleList, listOf(depictedItem))
@ -171,8 +194,21 @@ class CategoriesModelTest {
categoryItem("recentCategories"), categoryItem("recentCategories"),
), ),
) )
CategoriesModel(categoryClient, categoryDao, gpsCategoryModel)
.searchAll("", imageTitleList, listOf(depictedItemWithoutCategories))
.test()
.assertValue(
listOf(
categoryItem("categoriesOfP18"),
categoryItem("gpsCategory"),
categoryItem("titleSearch"),
categoryItem("recentCategories"),
),
)
imageTitleList.forEach { imageTitleList.forEach {
verify(categoryClient).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT) verify(categoryClient, times(2)).searchCategories(it, CategoriesModel.SEARCH_CATS_LIMIT)
verify(categoryClient).getCategoriesByName(any(), any(), any(), any())
verify(categoryClient).getCategoriesOfImage(any(), any())
} }
} }

View file

@ -132,6 +132,45 @@ class CategoryClientTest {
) )
} }
@Test
fun getCategoriesByTitlesFound() {
val mockResponse = withMockResponse("Category:Test")
whenever(
categoryInterface.getCategoriesByTitles(
anyString(),
anyInt(),
),
).thenReturn(Single.just(mockResponse))
categoryClient
.getCategoriesOfImage("tes", 10)
.test()
.assertValues(
listOf(
CategoryItem(
"Test",
"",
"",
false,
),
),
)
categoryClient
.getCategoriesOfImage(
"tes",
10,
).test()
.assertValues(
listOf(
CategoryItem(
"Test",
"",
"",
false,
),
),
)
}
@Test @Test
fun getCategoriesByNameNull() { fun getCategoriesByNameNull() {
val mockResponse = withNullPages() val mockResponse = withNullPages()
@ -160,6 +199,29 @@ class CategoryClientTest {
.assertValues(emptyList()) .assertValues(emptyList())
} }
@Test
fun getCategoriesByTitlesNull() {
val mockResponse = withNullPages()
whenever(
categoryInterface.getCategoriesByTitles(
anyString(),
anyInt(),
),
).thenReturn(Single.just(mockResponse))
categoryClient
.getCategoriesOfImage(
"tes",
10,
).test()
.assertValues(emptyList())
categoryClient
.getCategoriesOfImage(
"tes",
10,
).test()
.assertValues(emptyList())
}
@Test @Test
fun getParentCategoryListFound() { fun getParentCategoryListFound() {
val mockResponse = withMockResponse("Category:Test") val mockResponse = withMockResponse("Category:Test")

View file

@ -181,4 +181,20 @@ class DepictedItemTest {
fun `hashCode returns different values for objects with different name`() { fun `hashCode returns different values for objects with different name`() {
Assert.assertNotEquals(depictedItem(name = "a").hashCode(), depictedItem(name = "b").hashCode()) Assert.assertNotEquals(depictedItem(name = "a").hashCode(), depictedItem(name = "b").hashCode())
} }
@Test
fun `primaryImage is derived correctly from imageUrl`() {
Assert.assertEquals(
DepictedItem(
entity(
statements = mapOf(
WikidataProperties.IMAGE.propertyName to listOf(
statement(snak(dataValue = valueString("prefix: example_image name"))),
),
),
),
).primaryImage,
"_example_image_name",
)
}
} }