mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-27 12:53:55 +01:00
Merge branch 'main' into fix-multiupload
This commit is contained in:
commit
065fbd511d
39 changed files with 777 additions and 341 deletions
84
CHANGELOG.md
84
CHANGELOG.md
|
|
@ -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 doesn’t 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
|
||||||
|
|
|
||||||
|
|
@ -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\""
|
||||||
|
|
|
||||||
|
|
@ -262,4 +262,4 @@
|
||||||
android:required="false" />
|
android:required="false" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
229
app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
Normal file
229
app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 ?: "",
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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">الاختفاء هو <b>الملاذ الأخير</b> ويجب <b>استخدامه فقط عندما ترغب في التوقف عن التحرير إلى الأبد</b> وأيضًا لإخفاء أكبر عدد ممكن من ارتباطاتك السابقة.<br/><br/> يتم حذف الحساب على ويكيميديا كومنز عن طريق تغيير اسم حسابك بحيث لا يتمكن الآخرون من التعرف على مساهماتك في عملية تسمى اختفاء الحساب. <b>لا يضمن الاختفاء عدم الكشف عن الهوية تمامًا أو إزالة المساهمات في المشاريع</b> .</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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 wiki’s</string>
|
<string name="usages_on_other_wikis_heading">Andere wiki’s</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>
|
||||||
|
|
|
||||||
|
|
@ -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 <b>sista utväg</b> och bör <b>endast användas när du vill sluta redigera för alltid</b> och även dölja så många av dina tidigare associationer som möjligt.<br/><br/>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. <b>Försvinnande garanterar inte fullständig anonymitet eller att bidrag tas bort från projekten</b>.</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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue