diff --git a/CHANGELOG.md b/CHANGELOG.md index e7accf82b..df55b1124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Wikimedia Commons for Android +## v5.3.0 + +### What's changed +* Enable EmailAuth support +* Explore map images no longer show "Unknown" +* Fix crash when removing last two images of multiupload +* Mark ❌ for closed locations (P3999) in Nearby +* Fix two pin labels staying visible at the same time in Explore map +* Refactoring and minor UI improvements + +## v5.2.0 + +v5.2.0 boasts several new functionalities like: + +* A new refresh button lets you quickly reload the Nearby map +* Bookmarks now support categories +* Improved feedback and consistency in the user interface +* Bug fixes and performance improvements + +### What's changed +* Implement "Refresh" button to clear the cache and reload the Nearby map. +* `CommonsApplication` migrate to kotlin & some lint fixes. +* Revert back to MainScope for database and UI updates and make database operations thread safe. +* Hide edit options for logged-out users in Explore screen. +* Introduced a button to delete the current folder in custom selector. +* Improve Unique File Name Search. +* Migration of several modules from Java to Kotlin. +* Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins. +* Bug fixes and enhancement of Achievements screen. +* Show where file is being used on Commons and other wikis. +* Migrate android.media.ExifInterface to androidx.exifinterface.media.ExifInterface as android.media.ExifInterface had security flaws on older devices. +* Make dialogs modal and always show the upload icon. +* Fix unintentional deletion of subfolders and non-images by custom selector. +* Bookmark categories. +* Add pull down to refresh in the Contributions screen. +* Fix race condition and lag when loading pin details, faster overlay management. +* Show cached pins in Nearby even when internet is unavailable + + Full changelog with the list of contributors: [`v5.1.2...v5.2.0`](https://github.com/commons-app/apps-android-commons/compare/v5.1.2...v5.2.0). + + ## v5.1.2 ### What's changed diff --git a/app/build.gradle b/app/build.gradle index 6890177e8..34dd7f008 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -119,7 +119,8 @@ dependencies { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.10.0" testImplementation 'com.facebook.soloader:soloader:0.10.5' testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3" - debugImplementation("androidx.fragment:fragment-testing:1.6.2") + debugImplementation("androidx.fragment:fragment-testing-manifest:1.6.2") + androidTestImplementation("androidx.fragment:fragment-testing:1.6.2") testImplementation "commons-io:commons-io:2.6" // Android testing @@ -212,8 +213,8 @@ android { defaultConfig { //applicationId 'fr.free.nrw.commons' - versionCode 1046 - versionName '5.1.3' + versionCode 1051 + versionName '5.4.0' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion 21 @@ -318,7 +319,7 @@ android { 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", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\"" - buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"" + buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://commons-app.github.io/privacy-policy\"" buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"" buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\"" buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\"" @@ -333,6 +334,7 @@ android { buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\"" + buildConfigField "String", "CREATOR_PROPERTY", "\"P170\"" dimension 'tier' } @@ -355,7 +357,7 @@ android { 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", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\"" - buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\"" + buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://commons-app.github.io/privacy-policy\"" buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\"" buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\"" buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\"" @@ -370,6 +372,7 @@ android { buildConfigField "String", "TEST_USERNAME", "\"" + getTestUserName() + "\"" buildConfigField "String", "TEST_PASSWORD", "\"" + getTestPassword() + "\"" buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\"" + buildConfigField "String", "CREATOR_PROPERTY", "\"P253075\"" dimension 'tier' } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt b/app/src/androidTest/java/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt similarity index 100% rename from app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt rename to app/src/androidTest/java/fr/free/nrw/commons/contributions/ContributionsListFragmentUnitTests.kt diff --git a/app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt b/app/src/androidTest/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt similarity index 100% rename from app/src/test/kotlin/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt rename to app/src/androidTest/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragmentUnitTests.kt diff --git a/app/src/main/java/fr/free/nrw/commons/Media.kt b/app/src/main/java/fr/free/nrw/commons/Media.kt index 1222cd8b0..d07bc0265 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.kt +++ b/app/src/main/java/fr/free/nrw/commons/Media.kt @@ -53,6 +53,7 @@ class Media constructor( */ var author: String? = null, var user: String? = null, + var creatorName: String? = null, /** * Gets the categories the file falls under. * @return file categories as an ArrayList of Strings @@ -66,6 +67,7 @@ class Media constructor( var captions: Map = emptyMap(), var descriptions: Map = emptyMap(), var depictionIds: List = emptyList(), + var creatorIds: List = emptyList(), /** * This field was added to find non-hidden categories * Stores the mapping of category title to hidden attribute @@ -130,6 +132,7 @@ class Media constructor( * returns user * @return Author or User */ + @Deprecated("Use user for uploader username. Use attributedAuthor() for attribution. Note that the uploader may not be the creator/author.") fun getAuthorOrUser(): String? { return if (!author.isNullOrEmpty()) { author @@ -138,6 +141,19 @@ class Media constructor( } } + /** + * Returns author if it's not null or empty, otherwise + * returns creator name + * @return name of author or creator + */ + fun getAttributedAuthor(): String? { + return if (!author.isNullOrEmpty()) { + author + } else{ + creatorName + } + } + /** * Gets media display title * @return Media title diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt index 2ff54959d..970413283 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt +++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.kt @@ -1,7 +1,7 @@ package fr.free.nrw.commons import androidx.core.text.HtmlCompat -import fr.free.nrw.commons.media.IdAndCaptions +import fr.free.nrw.commons.media.IdAndLabels import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.PAGE_ID_PREFIX import io.reactivex.Single @@ -23,13 +23,23 @@ class MediaDataExtractor private val mediaClient: MediaClient, ) { fun fetchDepictionIdsAndLabels(media: Media) = - mediaClient + mediaClient .getEntities(media.depictionIds) .map { it .entities() .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } - }.map { it.map { (key, value) -> IdAndCaptions(key, value) } } + }.map { it.map { (key, value) -> IdAndLabels(key, value) } } + .onErrorReturn { emptyList() } + + fun fetchCreatorIdsAndLabels(media: Media) = + mediaClient + .getEntities(media.creatorIds) + .map { + it + .entities() + .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } + }.map { it.map { (key, value) -> IdAndLabels(key, value) } } .onErrorReturn { emptyList() } fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename) diff --git a/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java index 5ca20372a..b887aaf99 100644 --- a/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/ViewPagerAdapter.java @@ -18,6 +18,17 @@ public class ViewPagerAdapter extends FragmentPagerAdapter { super(manager); } + /** + * Constructs a ViewPagerAdapter with a specified Fragment Manager and Fragment resume behavior. + * + * @param manager The FragmentManager + * @param behavior An integer which represents the behavior of non visible fragments. See + * FragmentPagerAdapter.java for options. + */ + public ViewPagerAdapter(FragmentManager manager, int behavior) { + super(manager, behavior); + } + /** * This method returns the fragment of the viewpager at a particular position * @param position diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt index 75c4ac26d..840bc7ca3 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -65,6 +65,7 @@ class LoginActivity : AccountAuthenticatorActivity() { private val delegate: AppCompatDelegate by lazy { AppCompatDelegate.create(this, null) } + private var lastLoginResult: LoginResult? = null public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -271,6 +272,7 @@ class LoginActivity : AccountAuthenticatorActivity() { showLoggingProgressBar() loginClient.doLogin(username, password, + lastLoginResult, twoFactorCode, Locale.getDefault().language, object : LoginCallback { @@ -280,9 +282,17 @@ class LoginActivity : AccountAuthenticatorActivity() { onLoginSuccess(loginResult) } - override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread { + override fun twoFactorPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread { Timber.d("Requesting 2FA prompt") progressDialog!!.dismiss() + lastLoginResult = loginResult + askUserForTwoFactorAuth() + } + + override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) { + Timber.d("Requesting email auth prompt") + progressDialog!!.dismiss() + lastLoginResult = loginResult askUserForTwoFactorAuth() } @@ -341,12 +351,13 @@ class LoginActivity : AccountAuthenticatorActivity() { progressDialog!!.dismiss() with(binding!!) { twoFactorContainer.visibility = View.VISIBLE + twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code) loginTwoFactor.visibility = View.VISIBLE loginTwoFactor.requestFocus() } val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) - showMessageAndCancelDialog(R.string.login_failed_2fa_needed) + showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed)) } @VisibleForTesting diff --git a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt index f35e5f003..6353e54ac 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/csrf/CsrfTokenClient.kt @@ -32,7 +32,7 @@ class CsrfTokenClient( try { if (retry > 0) { // Log in explicitly - loginClient.loginBlocking(userName, password, "") + loginClient.loginBlocking(userName, password) } // Get CSRFToken response off the main thread. @@ -92,6 +92,8 @@ class CsrfTokenClient( override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } override fun twoFactorPrompt() = cb.twoFactorPrompt() + + override fun emailAuthPrompt() = cb.emailAuthPrompt() }, ) @@ -165,10 +167,17 @@ class CsrfTokenClient( } override fun twoFactorPrompt( + loginResult: LoginResult, caught: Throwable, token: String?, ) = callback.twoFactorPrompt() + override fun emailAuthPrompt( + loginResult: LoginResult, + caught: Throwable, + token: String?, + ) = callback.emailAuthPrompt() + // Should not happen here, but call the callback just in case. override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password.")) @@ -190,6 +199,8 @@ class CsrfTokenClient( fun failure(caught: Throwable?) fun twoFactorPrompt() + + fun emailAuthPrompt() } companion object { diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt index 8092f73ae..8aa3d17a0 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginCallback.kt @@ -4,6 +4,13 @@ interface LoginCallback { fun success(loginResult: LoginResult) fun twoFactorPrompt( + loginResult: LoginResult, + caught: Throwable, + token: String?, + ) + + fun emailAuthPrompt( + loginResult: LoginResult, caught: Throwable, token: String?, ) diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt index 2a799c847..a653b8b55 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginClient.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.auth.login import android.text.TextUtils +import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL @@ -51,6 +52,7 @@ class LoginClient( password, null, null, + null, response.body()!!.query()!!.loginToken(), userLanguage, cb, @@ -75,6 +77,7 @@ class LoginClient( password: String, retypedPassword: String?, twoFactorCode: String?, + emailAuthCode: String?, loginToken: String?, userLanguage: String, cb: LoginCallback, @@ -82,7 +85,7 @@ class LoginClient( this.userLanguage = userLanguage loginCall = - if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { + if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) { loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) } else { loginInterface.postLogIn( @@ -90,6 +93,7 @@ class LoginClient( password, retypedPassword, twoFactorCode, + emailAuthCode, loginToken, userLanguage, true, @@ -112,10 +116,18 @@ class LoginClient( when (loginResult) { is OAuthResult -> cb.twoFactorPrompt( + loginResult, LoginFailedException(loginResult.message), loginToken, ) + is EmailAuthResult -> + cb.emailAuthPrompt( + loginResult, + LoginFailedException(loginResult.message), + loginToken + ) + is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) is LoginResult.Result -> @@ -147,6 +159,7 @@ class LoginClient( fun doLogin( username: String, password: String, + lastLoginResult: LoginResult?, twoFactorCode: String, userLanguage: String, loginCallback: LoginCallback, @@ -159,7 +172,10 @@ class LoginClient( ) = if (response.isSuccessful) { val loginToken = response.body()?.query()?.loginToken() loginToken?.let { - login(username, password, null, twoFactorCode, it, userLanguage, loginCallback) + login(username, password, null, + if (lastLoginResult is OAuthResult) twoFactorCode else null, + if (lastLoginResult is EmailAuthResult) twoFactorCode else null, + it, userLanguage, loginCallback) } ?: run { loginCallback.error(IOException("Failed to retrieve login token")) } @@ -181,7 +197,8 @@ class LoginClient( fun loginBlocking( userName: String, password: String, - twoFactorCode: String?, + twoFactorCode: String? = null, + emailAuthCode: String? = null ) { val tokenResponse = getLoginToken().execute() if (tokenResponse @@ -195,7 +212,7 @@ class LoginClient( val loginToken = tokenResponse.body()?.query()?.loginToken() val tempLoginCall = - if (twoFactorCode.isNullOrEmpty()) { + if (twoFactorCode.isNullOrEmpty() && emailAuthCode.isNullOrEmpty()) { loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) } else { loginInterface.postLogIn( @@ -203,6 +220,7 @@ class LoginClient( password, null, twoFactorCode, + emailAuthCode, loginToken, userLanguage, true, @@ -214,7 +232,7 @@ class LoginClient( val loginResult = loginResponse.toLoginResult(password) ?: throw IOException("Unexpected response when logging in.") if ("UI" == loginResult.status) { - if (loginResult is OAuthResult) { + if (loginResult is OAuthResult || loginResult is EmailAuthResult) { // TODO: Find a better way to boil up the warning about 2FA throw LoginFailedException(loginResult.message) } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt index 07e1cd45c..39cbf7c9f 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginInterface.kt @@ -35,7 +35,8 @@ interface LoginInterface { @Field("password") pass: String?, @Field("retype") retypedPass: String?, @Field("OATHToken") twoFactorCode: String?, - @Field("logintoken") token: String?, + @Field("token") emailAuthToken: String?, + @Field("logintoken") loginToken: String?, @Field("uselang") userLanguage: String?, @Field("logincontinue") loginContinue: Boolean, ): Call diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt index a96778e38..0fb035eea 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResponse.kt @@ -2,6 +2,7 @@ package fr.free.nrw.commons.auth.login import com.google.gson.annotations.SerializedName import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult +import fr.free.nrw.commons.auth.login.LoginResult.EmailAuthResult import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult import fr.free.nrw.commons.auth.login.LoginResult.Result import fr.free.nrw.commons.wikidata.mwapi.MwServiceError @@ -27,11 +28,13 @@ internal class ClientLogin { fun toLoginResult(password: String): LoginResult { var userMessage = message if ("UI" == status) { - if (requests != null) { - for (req in requests) { - if ("MediaWiki\\Extension\\OATHAuth\\Auth\\TOTPAuthenticationRequest" == req.id()) { + requests?.forEach { request -> + request.id()?.let { + if (it.endsWith("TOTPAuthenticationRequest")) { return OAuthResult(status, userName, password, message) - } else if ("MediaWiki\\Auth\\PasswordAuthenticationRequest" == req.id()) { + } else if (it.endsWith("EmailAuthAuthenticationRequest")) { + return EmailAuthResult(status, userName, password, message) + } else if (it.endsWith("PasswordAuthenticationRequest")) { return ResetPasswordResult(status, userName, password, message) } } @@ -49,7 +52,7 @@ internal class Request { private val required: String? = null private val provider: String? = null private val account: String? = null - private val fields: Map? = null + internal val fields: Map? = null fun id(): String? = id } @@ -57,5 +60,5 @@ internal class Request { internal class RequestField { private val type: String? = null private val label: String? = null - private val help: String? = null + internal val help: String? = null } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt index 6a7594ec0..99abaeeec 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/login/LoginResult.kt @@ -24,6 +24,13 @@ sealed class LoginResult( message: String?, ) : LoginResult(status, userName, password, message) + class EmailAuthResult( + status: String, + userName: String?, + password: String?, + message: String?, + ) : LoginResult(status, userName, password, message) + class ResetPasswordResult( status: String, userName: String?, diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt index 4ab21462c..f10e02ebc 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsFragment.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.bookmarks.locations import android.Manifest.permission -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View @@ -9,15 +8,12 @@ import android.view.ViewGroup import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager import dagger.android.support.DaggerFragment import fr.free.nrw.commons.R import fr.free.nrw.commons.contributions.ContributionController import fr.free.nrw.commons.databinding.FragmentBookmarksLocationsBinding -import fr.free.nrw.commons.filepicker.FilePicker import fr.free.nrw.commons.nearby.Place import fr.free.nrw.commons.nearby.fragments.CommonPlaceClickActions import fr.free.nrw.commons.nearby.fragments.PlaceAdapter @@ -41,33 +37,27 @@ class BookmarkLocationsFragment : DaggerFragment() { private val cameraPickLauncherForResult = registerForActivityResult(StartActivityForResult()) { result -> contributionController.handleActivityResultWithCallback( - requireActivity(), - object: FilePicker.HandleActivityResult { - override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { - contributionController.onPictureReturnedFromCamera( - result, - requireActivity(), - callbacks - ) - } - } - ) + requireActivity() + ) { callbacks -> + contributionController.onPictureReturnedFromCamera( + result, + requireActivity(), + callbacks + ) + } } private val galleryPickLauncherForResult = registerForActivityResult(StartActivityForResult()) { result -> contributionController.handleActivityResultWithCallback( - requireActivity(), - object: FilePicker.HandleActivityResult { - override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { - contributionController.onPictureReturnedFromGallery( - result, - requireActivity(), - callbacks - ) - } - } - ) + requireActivity() + ) { callbacks -> + contributionController.onPictureReturnedFromGallery( + result, + requireActivity(), + callbacks + ) + } } companion object { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt index 32028cfd2..899ef458f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionViewHolder.kt @@ -8,23 +8,29 @@ import androidx.appcompat.app.AlertDialog import androidx.recyclerview.widget.RecyclerView import com.facebook.imagepipeline.request.ImageRequest import com.facebook.imagepipeline.request.ImageRequestBuilder +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.utils.MediaAttributionUtil +import fr.free.nrw.commons.MediaDataExtractor import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.LayoutContributionBinding import fr.free.nrw.commons.media.MediaClient import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.schedulers.Schedulers +import timber.log.Timber import java.io.File class ContributionViewHolder internal constructor( - private val parent: View, private val callback: ContributionsListAdapter.Callback, - private val mediaClient: MediaClient + parent: View, + private val callback: ContributionsListAdapter.Callback, + private val compositeDisposable: CompositeDisposable, + private val mediaClient: MediaClient, + private val mediaDataExtractor: MediaDataExtractor ) : RecyclerView.ViewHolder(parent) { var binding: LayoutContributionBinding = LayoutContributionBinding.bind(parent) private var position = 0 private var contribution: Contribution? = null - private val compositeDisposable = CompositeDisposable() private var isWikipediaButtonDisplayed = false private val pausingPopUp: AlertDialog var imageRequest: ImageRequest? = null @@ -54,7 +60,7 @@ an upload might take a dozen seconds. */ this.contribution = contribution this.position = position binding.contributionTitle.text = contribution.media.mostRelevantCaption - binding.authorView.text = contribution.media.getAuthorOrUser() + setAuthorText(contribution.media) //Removes flicker of loading image. binding.contributionImage.hierarchy.fadeDuration = 0 @@ -93,6 +99,30 @@ an upload might take a dozen seconds. */ checkIfMediaExistsOnWikipediaPage(contribution) } + fun updateAttribution() { + if (contribution != null) { + val media = contribution!!.media + if (!media.getAttributedAuthor().isNullOrEmpty()) { + return + } + compositeDisposable.addAll( + mediaDataExtractor.fetchCreatorIdsAndLabels(media) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { idAndLabels -> + media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels) + setAuthorText(media) + }, + { t: Throwable? -> Timber.e(t) }) + ) + } + } + + private fun setAuthorText(media: Media) { + binding.authorView.text = MediaAttributionUtil.getTagLine(media, itemView.context) + } + /** * Checks if a media exists on the corresponding Wikipedia article Currently the check is made * for the device's current language Wikipedia diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt index b41de1c6e..e5f721721 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListAdapter.kt @@ -4,21 +4,26 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil +import fr.free.nrw.commons.MediaDataExtractor import fr.free.nrw.commons.R import fr.free.nrw.commons.media.MediaClient +import io.reactivex.disposables.CompositeDisposable /** * Represents The View Adapter for the List of Contributions */ class ContributionsListAdapter internal constructor( private val callback: Callback, - private val mediaClient: MediaClient + private val mediaClient: MediaClient, + private val mediaDataExtractor: MediaDataExtractor, + private val compositeDisposable: CompositeDisposable ) : PagedListAdapter(DIFF_CALLBACK) { /** * Initializes the view holder with contribution data */ override fun onBindViewHolder(holder: ContributionViewHolder, position: Int) { holder.init(position, getItem(position)) + holder.updateAttribution() } fun getContributionForPosition(position: Int): Contribution? { @@ -36,7 +41,7 @@ class ContributionsListAdapter internal constructor( val viewHolder = ContributionViewHolder( LayoutInflater.from(parent.context) .inflate(R.layout.layout_contribution, parent, false), - callback, mediaClient + callback, compositeDisposable, mediaClient, mediaDataExtractor ) return viewHolder } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt index bfe1161c7..9ecb35b24 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.kt @@ -27,6 +27,7 @@ import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver import androidx.recyclerview.widget.RecyclerView.OnItemTouchListener import androidx.recyclerview.widget.SimpleItemAnimator import fr.free.nrw.commons.Media +import fr.free.nrw.commons.MediaDataExtractor import fr.free.nrw.commons.R import fr.free.nrw.commons.Utils import fr.free.nrw.commons.auth.SessionManager @@ -63,6 +64,10 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL @Inject var mediaClient: MediaClient? = null + @JvmField + @Inject + var mediaDataExtractor: MediaDataExtractor? = null + @JvmField @Named(NetworkingModule.NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) @Inject @@ -231,7 +236,7 @@ class ContributionsListFragment : CommonsDaggerSupportFragment(), ContributionsL } private fun initAdapter() { - adapter = ContributionsListAdapter(this, mediaClient!!) + adapter = ContributionsListAdapter(this, mediaClient!!, mediaDataExtractor!!, compositeDisposable) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt index a61567393..a83532bdb 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.kt @@ -387,35 +387,40 @@ after opening the app. } override fun onBackPressed() { - if (contributionsFragment != null && activeFragment == ActiveFragment.CONTRIBUTIONS) { + when (activeFragment) { + ActiveFragment.CONTRIBUTIONS -> { // Means that contribution fragment is visible - if (!contributionsFragment!!.backButtonClicked()) { //If this one does not wan't to handle + if (contributionsFragment?.backButtonClicked() != true) { //If this one does not want to handle // the back press, let the activity do so super.onBackPressed() + } } - } else if (nearbyParentFragment != null && activeFragment == ActiveFragment.NEARBY) { + ActiveFragment.NEARBY -> { // Means that nearby fragment is visible - /* If function nearbyParentFragment.backButtonClick() returns false, it means that the bottomsheet is - not expanded. So if the back button is pressed, then go back to the Contributions tab */ - if (!nearbyParentFragment!!.backButtonClicked()) { - supportFragmentManager.beginTransaction().remove(nearbyParentFragment!!) - .commit() + if (nearbyParentFragment?.backButtonClicked() != true) { + nearbyParentFragment?.let { + supportFragmentManager.beginTransaction().remove(it).commit() + } setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + } } - } else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) { - // Means that explore fragment is visible - if (!exploreFragment!!.onBackPressed()) { - if (applicationKvStore!!.getBoolean("login_skipped")) { + ActiveFragment.EXPLORE -> { + // Explore Fragment is visible + if (exploreFragment?.onBackPressed() != true) { + if (applicationKvStore?.getBoolean("login_skipped") == true) { super.onBackPressed() } else { setSelectedItemId(NavTab.CONTRIBUTIONS.code()) + } } } - } else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) { + ActiveFragment.BOOKMARK -> { // Means that bookmark fragment is visible - bookmarkFragment!!.onBackPressed() - } else { + bookmarkFragment?.onBackPressed() + } + else -> { super.onBackPressed() + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt index ff623d496..20a2fe70a 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/adapter/ImageAdapter.kt @@ -26,6 +26,7 @@ import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.TreeMap import kotlin.collections.ArrayList @@ -342,45 +343,36 @@ class ImageAdapter( numberOfSelectedImagesMarkedAsNotForUpload-- } notifyItemChanged(position, ImageUnselected()) - - // Getting index from all images index when switch is on - val indexes = - if (showAlreadyActionedImages) { - ImageHelper.getIndexList(selectedImages, images) - - // Getting index from actionable images when switch is off - } else { - ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values)) - } - for (index in indexes) { - notifyItemChanged(index, ImageSelectedOrUpdated()) - } } else { - if (holder.isItemUploaded()) { - Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() - } else { - if (holder.isItemNotForUpload()) { - numberOfSelectedImagesMarkedAsNotForUpload++ - } - - // Getting index from all images index when switch is on - val indexes: ArrayList = - if (showAlreadyActionedImages) { - selectedImages.add(images[position]) - ImageHelper.getIndexList(selectedImages, images) - - // Getting index from actionable images when switch is off - } else { - selectedImages.add(ArrayList(actionableImagesMap.values)[position]) - ImageHelper.getIndexList(selectedImages, ArrayList(actionableImagesMap.values)) + val image = images[position] + scope.launch(ioDispatcher) { + val imageSHA1 = imageLoader.getSHA1(image, defaultDispatcher) + withContext(Dispatchers.Main) { + if (holder.isItemUploaded()) { + Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() + return@withContext } - for (index in indexes) { - notifyItemChanged(index, ImageSelectedOrUpdated()) + if (imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) { + holder.itemUploaded() + Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() + return@withContext + } + + if (!holder.isItemUploaded() && imageSHA1.isNotEmpty() && imageLoader.getFromUploaded(imageSHA1) != null) { + Toast.makeText(context, R.string.custom_selector_already_uploaded_image_text, Toast.LENGTH_SHORT).show() + } + + if (holder.isItemNotForUpload()) { + numberOfSelectedImagesMarkedAsNotForUpload++ + } + selectedImages.add(image) + notifyItemChanged(position, ImageSelectedOrUpdated()) + + imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) } } } - imageSelectListener.onSelectedImagesChanged(selectedImages, numberOfSelectedImagesMarkedAsNotForUpload) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 4e2d58bab..6b78dfd41 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -638,17 +638,20 @@ class CustomSelectorActivity : finishPickImages(arrayListOf()) return } - var i = 0 - while (i < selectedImages.size) { - val path = selectedImages[i].path - val file = File(path) - if (!file.exists()) { - selectedImages.removeAt(i) - i-- + scope.launch(ioDispatcher) { + val uniqueImages = selectedImages.distinctBy { image -> + CustomSelectorUtils.getImageSHA1( + image.uri, + ioDispatcher, + fileUtilsWrapper, + contentResolver + ) + } + + withContext(Dispatchers.Main) { + finishPickImages(ArrayList(uniqueImages)) } - i++ } - finishPickImages(selectedImages) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java index 223d028dc..b31c34b67 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java @@ -12,6 +12,7 @@ import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentPagerAdapter; import androidx.viewpager.widget.ViewPager.OnPageChangeListener; import fr.free.nrw.commons.R; import fr.free.nrw.commons.ViewPagerAdapter; @@ -69,7 +70,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment { loadNearbyMapData(); binding = FragmentExploreBinding.inflate(inflater, container, false); - viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager()); + viewPagerAdapter = new ViewPagerAdapter(getChildFragmentManager(), + FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + binding.viewPager.setAdapter(viewPagerAdapter); binding.viewPager.setId(R.id.viewPager); binding.tabLayout.setupWithViewPager(binding.viewPager); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java index 1b1659182..194ed5d24 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java @@ -120,6 +120,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment private double prevZoom; private double prevLatitude; private double prevLongitude; + private boolean recentlyCameFromNearbyMap; private ExploreMapPresenter presenter; @@ -221,7 +222,6 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment binding.mapView.getController().setZoom(ZOOM_LEVEL); } - performMapReadyActions(); binding.mapView.getOverlays().add(new MapEventsOverlay(new MapEventsReceiver() { @Override @@ -341,7 +341,12 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment !locationPermissionsHelper.checkLocationPermission(getActivity())) { isPermissionDenied = true; } - lastKnownLocation = MapUtils.getDefaultLatLng(); + + lastKnownLocation = getLastLocation(); + + if (lastKnownLocation == null) { + lastKnownLocation = MapUtils.getDefaultLatLng(); + } // if we came from 'Show in Explore' in Nearby, load Nearby map center and zoom if (isCameFromNearbyMap()) { @@ -367,6 +372,16 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment prevLatitude = getArguments().getDouble("prev_latitude"); prevLongitude = getArguments().getDouble("prev_longitude"); } + + setRecentlyCameFromNearbyMap(isCameFromNearbyMap()); + } + + /** + * @return The LatLng from the previous Fragment's map center or (0,0,0) coordinates + * if that information is not available/applicable. + */ + public LatLng getPreviousLatLng() { + return new LatLng(prevLatitude, prevLongitude, (float)prevZoom); } /** @@ -379,6 +394,23 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0; } + /** + * Gets the value that indicates if the user navigated from "Show in Explore" in Nearby and + * that the LatLng from Nearby has yet to be searched for map markers. + */ + public boolean recentlyCameFromNearbyMap() { + return recentlyCameFromNearbyMap; + } + + /** + * Sets the value that indicates if the user navigated from "Show in Explore" in Nearby and + * that the LatLng from Nearby has yet to be searched for map markers. + * @param newValue The value to set. + */ + public void setRecentlyCameFromNearbyMap(boolean newValue) { + recentlyCameFromNearbyMap = newValue; + } + public void loadNearbyMapFromExplore() { ((MainActivity) getContext()).loadNearbyMapFromExplore( binding.mapView.getZoomLevelDouble(), @@ -708,8 +740,29 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment GeoPoint point = new GeoPoint( nearbyBaseMarker.getPlace().location.getLatitude(), nearbyBaseMarker.getPlace().location.getLongitude()); - OverlayItem item = new OverlayItem(nearbyBaseMarker.getPlace().name, null, - point); + + Media markerMedia = this.getMediaFromImageURL(nearbyBaseMarker.getPlace().pic); + String authorUser = null; + if (markerMedia != null) { + authorUser = markerMedia.getAuthorOrUser(); + // HTML text is sometimes part of the author string and needs to be removed + authorUser = Html.fromHtml(authorUser, Html.FROM_HTML_MODE_LEGACY).toString(); + } + + String title = nearbyBaseMarker.getPlace().name; + // Remove "File:" if present at start + if (title.startsWith("File:")) { + title = title.substring(5); + } + // Remove extensions like .jpg, .jpeg, .png, .svg (case insensitive) + title = title.replaceAll("(?i)\\.(jpg|jpeg|png|svg)$", ""); + title = title.replace("_", " "); + //Truncate if too long because it doesn't fit the screen + if (title.length() > 43) { + title = title.substring(0, 40) + "…"; + } + + OverlayItem item = new OverlayItem(title, authorUser, point); item.setMarker(d); items.add(item); ItemizedOverlayWithFocus overlay = new ItemizedOverlayWithFocus(items, @@ -740,13 +793,37 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment } } + /** + * Retrieves the specific Media object from the mediaList field. + * @param url The specific Media's image URL. + * @return The Media object that matches the URL or null if it could not be found. + */ + private Media getMediaFromImageURL(String url) { + if (mediaList == null || url == null) { + return null; + } + + for (int i = 0; i < mediaList.size(); i++) { + if (mediaList.get(i) != null && mediaList.get(i).getImageUrl() != null + && mediaList.get(i).getImageUrl().equals(url)) { + return mediaList.get(i); + } + } + + return null; + } + /** * Removes a marker from the map based on the specified NearbyBaseMarker. * * @param nearbyBaseMarker The NearbyBaseMarker object representing the marker to be removed. */ private void removeMarker(BaseMarker nearbyBaseMarker) { - Place place = nearbyBaseMarker.getPlace(); + if (nearbyBaseMarker == null || nearbyBaseMarker.getPlace().getName() == null) { + return; + } + + String target = nearbyBaseMarker.getPlace().getName(); List overlays = binding.mapView.getOverlays(); ItemizedOverlayWithFocus item; @@ -755,8 +832,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment item = (ItemizedOverlayWithFocus) overlays.get(i); OverlayItem overlayItem = item.getItem(0); - if (place.location.getLatitude() == overlayItem.getPoint().getLatitude() - && place.location.getLongitude() == overlayItem.getPoint().getLongitude()) { + if (overlayItem.getTitle().equals(target)) { binding.mapView.getOverlays().remove(i); binding.mapView.invalidate(); break; @@ -930,9 +1006,6 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment -0.07483536015053005, 1f); } } - if (!isCameFromNearbyMap()) { - moveCameraToPosition(new GeoPoint(latLnge.getLatitude(), latLnge.getLongitude())); - } return latLnge; } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java index 94b9cf5ad..45a11a422 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapPresenter.java @@ -74,9 +74,22 @@ public class ExploreMapPresenter */ if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)) { Timber.d("LOCATION_SIGNIFICANTLY_CHANGED"); + LatLng populateLatLng = exploreMapFragmentView.getMapCenter(); + + //If "Show in Explore" was selected in Nearby, use the previous LatLng + if (exploreMapFragmentView instanceof ExploreMapFragment) { + ExploreMapFragment exploreMapFragment = (ExploreMapFragment)exploreMapFragmentView; + if (exploreMapFragment.recentlyCameFromNearbyMap()) { + //Ensure this LatLng will not be used again if user searches their GPS location + exploreMapFragment.setRecentlyCameFromNearbyMap(false); + + populateLatLng = exploreMapFragment.getPreviousLatLng(); + } + } + lockUnlockNearby(true); exploreMapFragmentView.setProgressBarVisibility(true); - exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapCenter()); + exploreMapFragmentView.populatePlaces(populateLatLng); } else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) { Timber.d("SEARCH_CUSTOM_AREA"); lockUnlockNearby(true); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt index 0cfb270a3..fe5c21a7e 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/MediaConverter.kt @@ -18,6 +18,12 @@ import javax.inject.Inject class MediaConverter @Inject constructor() { + /** + * Creating Media object from MWQueryPage. + * + * @param page response from the API + * @return Media object + */ fun convert( page: MwQueryPage, entity: Entities.Entity, @@ -40,24 +46,17 @@ class MediaConverter metadata.prefixedLicenseUrl, getAuthor(metadata), imageInfo.getUser(), + null, MediaDataExtractorUtil.extractCategoriesFromList(metadata.categories()), metadata.latLng, entity.labels().mapValues { it.value.value() }, entity.descriptions().mapValues { it.value.value() }, entity.depictionIds(), + entity.creatorIds(), myMap, ) } - /** - * Creating Media object from MWQueryPage. - * Earlier only basic details were set for the media object but going forward, - * a full media object(with categories, descriptions, coordinates etc) can be constructed using this method - * - * @param page response from the API - * @return Media object - */ - private fun safeParseDate(dateStr: String): Date? = try { CommonsDateUtil.getMediaSimpleDateFormat().parse(dateStr) @@ -66,24 +65,32 @@ class MediaConverter } /** - * This method extracts the Commons Username from the artist HTML information + * This method extracts the Commons Username from the artist HTML information. + * When the HTML is in customized formatting, it may fail to parse and return null. * @param metadata * @return */ private fun getAuthor(metadata: ExtMetadata): String? { - return try { - val authorHtml = metadata.artist() - val anchorStartTagTerminalChars = "\">" - val anchorCloseTag = "" + val authorHtml = metadata.artist() + val anchorStartTagTerminalString = "\">" + val anchorCloseTag = "" - return authorHtml.substring( - authorHtml.indexOf(anchorStartTagTerminalChars) + - anchorStartTagTerminalChars - .length, + return if (!authorHtml.contains("<") && !authorHtml.contains(">") ) { + authorHtml.trim() + } else if (!authorHtml.contains(anchorStartTagTerminalString) || !authorHtml.endsWith(anchorCloseTag)) { + null + } else { + + val authorText = authorHtml.substring( + authorHtml.indexOf(anchorStartTagTerminalString) + + anchorStartTagTerminalString.length, authorHtml.indexOf(anchorCloseTag), ) - } catch (ex: java.lang.Exception) { - "" + if (authorText.contains("<") || authorText.contains(">")) { + null + } else { + authorText + } } } } @@ -92,6 +99,10 @@ private fun Entities.Entity.depictionIds() = this[WikidataProperties.DEPICTS]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id } ?: emptyList() +private fun Entities.Entity.creatorIds() = + this[WikidataProperties.CREATOR]?.mapNotNull { (it.mainSnak.dataValue as? DataValue.EntityId)?.value?.id } + ?: emptyList() + private val ExtMetadata.prefixedLicenseUrl: String get() = licenseUrl().let { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt index 987f4ca00..e19b1b056 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PageableMediaFragment.kt @@ -4,16 +4,18 @@ import android.content.Context import android.os.Bundle import android.view.View import fr.free.nrw.commons.Media +import fr.free.nrw.commons.MediaDataExtractor import fr.free.nrw.commons.R import fr.free.nrw.commons.category.CategoryImagesCallback import fr.free.nrw.commons.explore.paging.BasePagingFragment import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider +import javax.inject.Inject abstract class PageableMediaFragment : BasePagingFragment(), MediaDetailProvider { override val pagedListAdapter by lazy { - PagedMediaAdapter(categoryImagesCallback::onMediaClicked) + PagedMediaAdapter(categoryImagesCallback::onMediaClicked, mediaDataExtractor) } override val errorTextId: Int = R.string.error_loading_images @@ -22,6 +24,9 @@ abstract class PageableMediaFragment : lateinit var categoryImagesCallback: CategoryImagesCallback + @Inject + lateinit var mediaDataExtractor: MediaDataExtractor + override fun onAttach(context: Context) { super.onAttach(context) if (parentFragment != null) { diff --git a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt index 364b5d363..521ba77c6 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/media/PagedMediaAdapter.kt @@ -5,13 +5,22 @@ import android.view.ViewGroup import androidx.paging.PagedListAdapter import androidx.recyclerview.widget.DiffUtil import fr.free.nrw.commons.Media +import fr.free.nrw.commons.MediaDataExtractor +import fr.free.nrw.commons.utils.MediaAttributionUtil import fr.free.nrw.commons.R import fr.free.nrw.commons.databinding.LayoutCategoryImagesBinding import fr.free.nrw.commons.explore.paging.BaseViewHolder import fr.free.nrw.commons.explore.paging.inflate +import fr.free.nrw.commons.media.IdAndLabels +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber class PagedMediaAdapter( private val onImageClicked: (Int) -> Unit, + private val mediaDataExtractor: MediaDataExtractor, + private val compositeDisposable: CompositeDisposable = CompositeDisposable() ) : PagedListAdapter( object : DiffUtil.ItemCallback() { override fun areItemsTheSame( @@ -25,6 +34,7 @@ class PagedMediaAdapter( ) = oldItem.pageId == newItem.pageId }, ) { + override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, @@ -37,7 +47,24 @@ class PagedMediaAdapter( holder: SearchImagesViewHolder, position: Int, ) { - holder.bind(getItem(position)!! to position) + val media = getItem(position) ?: return + holder.bind(media to position) + + if (!media.getAttributedAuthor().isNullOrEmpty()) { + return + } + + compositeDisposable.addAll( + mediaDataExtractor.fetchCreatorIdsAndLabels(media) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { idAndLabels -> + media.creatorName = MediaAttributionUtil.getCreatorName(idAndLabels); + holder.setAuthorText(media) + }, + { t: Throwable? -> Timber.e(t) }) + ) } } @@ -52,7 +79,10 @@ class SearchImagesViewHolder( binding.categoryImageView.setOnClickListener { onImageClicked(item.second) } binding.categoryImageTitle.text = media.mostRelevantCaption binding.categoryImageView.setImageURI(media.thumbUrl) - binding.categoryImageAuthor.text = - containerView.context.getString(R.string.image_uploaded_by, media.getAuthorOrUser()) + setAuthorText(media) + } + + fun setAuthorText(media: Media) { + binding.categoryImageAuthor.text = MediaAttributionUtil.getTagLine(media, containerView.context) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/paging/FooterAdapter.kt b/app/src/main/java/fr/free/nrw/commons/explore/paging/FooterAdapter.kt index fc5f529b2..d739b35ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/paging/FooterAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/paging/FooterAdapter.kt @@ -32,7 +32,7 @@ class FooterAdapter( override fun onCreateViewHolder( parent: ViewGroup, viewType: Int, - ) = when (FooterItem.values()[viewType]) { + ) = when (FooterItem.entries[viewType]) { FooterItem.LoadingItem -> LoadingViewHolder( parent.inflate(R.layout.list_item_progress), diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt deleted file mode 100644 index fe96eb8cb..000000000 --- a/app/src/main/java/fr/free/nrw/commons/media/IdAndCaptions.kt +++ /dev/null @@ -1,6 +0,0 @@ -package fr.free.nrw.commons.media - -data class IdAndCaptions( - val id: String, - val captions: Map, -) diff --git a/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt new file mode 100644 index 000000000..c989ee7e3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/media/IdAndLabels.kt @@ -0,0 +1,18 @@ +package fr.free.nrw.commons.media + +data class IdAndLabels( + val id: String, + val labels: Map, +) { + // if a label is available in user's locale, return it + // if not then check for english, else show any available. + fun getLocalizedLabel(locale: String): String? { + if (labels[locale] != null) { + return labels[locale] + } + if (labels["en"] != null) { + return labels["en"] + } + return labels.values.firstOrNull() ?: id + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt index 77ff1df0c..8a4d530c4 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt @@ -16,7 +16,6 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.ViewTreeObserver import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.widget.ArrayAdapter import android.widget.Button @@ -622,10 +621,9 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) }, + { idAndCaptions: List -> onDepictionsLoaded(idAndCaptions) }, { t: Throwable? -> Timber.e(t) }) ) - // compositeDisposable.add(disposable); } private fun onDiscussionLoaded(discussion: String) { @@ -655,7 +653,7 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C } } - private fun onDepictionsLoaded(idAndCaptions: List) { + private fun onDepictionsLoaded(idAndCaptions: List) { binding.depictsLayout.visibility = View.VISIBLE binding.depictionsEditButton.visibility = View.VISIBLE buildDepictionList(idAndCaptions) @@ -865,24 +863,24 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C * Populates media details fragment with depiction list * @param idAndCaptions */ - private fun buildDepictionList(idAndCaptions: List) { + private fun buildDepictionList(idAndCaptions: List) { binding.mediaDetailDepictionContainer.removeAllViews() // Create a mutable list from the original list val mutableIdAndCaptions = idAndCaptions.toMutableList() if (mutableIdAndCaptions.isEmpty()) { - // Create a placeholder IdAndCaptions object and add it to the list + // Create a placeholder IdAndLabels object and add it to the list mutableIdAndCaptions.add( - IdAndCaptions( + IdAndLabels( id = media?.pageId ?: "", // Use an empty string if media?.pageId is null - captions = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value + labels = mapOf(Locale.getDefault().language to getString(R.string.detail_panel_cats_none)) // Create a Map with the language as the key and the message as the value ) ) } val locale: String = Locale.getDefault().language - for (idAndCaption: IdAndCaptions in mutableIdAndCaptions) { + for (idAndCaption in mutableIdAndCaptions) { binding.mediaDetailDepictionContainer.addView( buildDepictLabel( getDepictionCaption(idAndCaption, locale), @@ -894,16 +892,16 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C } - private fun getDepictionCaption(idAndCaption: IdAndCaptions, locale: String): String? { + private fun getDepictionCaption(idAndCaption: IdAndLabels, locale: String): String? { // Check if the Depiction Caption is available in user's locale // if not then check for english, else show any available. - if (idAndCaption.captions[locale] != null) { - return idAndCaption.captions[locale] + if (idAndCaption.labels[locale] != null) { + return idAndCaption.labels[locale] } - if (idAndCaption.captions["en"] != null) { - return idAndCaption.captions["en"] + if (idAndCaption.labels["en"] != null) { + return idAndCaption.labels["en"] } - return idAndCaption.captions.values.iterator().next() + return idAndCaption.labels.values.iterator().next() } private fun onMediaDetailLicenceClicked() { diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt index ef0ef1f9c..643374e54 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaInterface.kt @@ -186,13 +186,25 @@ interface MediaInterface { ): Single companion object { + /** + * Retrieved thumbnail height will be about this tall, but must be at least this height. + * A larger number means higher thumbnail resolution but more network usage. + */ + const val THUMB_HEIGHT_PX = 450 + const val MEDIA_PARAMS = - "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl" + "&prop=imageinfo|coordinates&iiprop=url|extmetadata|user&&iiurlheight=" + + THUMB_HEIGHT_PX + + "&iiextmetadatafilter=DateTime|Categories|GPSLatitude|GPSLongitude|" + + "ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl" /** * fetches category detail(title, hidden) for each category along with File information */ const val MEDIA_PARAMS_WITH_CATEGORY_DETAILS = - "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlwidth=640&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl" + "&clprop=hidden&prop=categories|imageinfo&iiprop=url|extmetadata|user&&iiurlheight=" + + THUMB_HEIGHT_PX + + "&iiextmetadatafilter=DateTime|GPSLatitude|GPSLongitude|ImageDescription|" + + "DateTimeOriginal|Artist|LicenseShortName|LicenseUrl" } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 3b3b798eb..cff2ed4de 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -153,7 +153,10 @@ public class Place implements Parcelable { .build(), item.getPic().getValue(), // Checking if the place exists or not - (item.getDestroyed().getValue() == "") && (item.getEndTime().getValue() == ""), entityId); + (item.getDestroyed().getValue() == "") && (item.getEndTime().getValue() == "") + && (item.getDateOfOfficialClosure().getValue() == "") + && (item.getPointInTime().getValue()==""), + entityId); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt index e5196bee8..1775401dd 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/WikidataFeedback.kt @@ -63,7 +63,10 @@ class WikidataFeedback : BaseActivity() { supportActionBar!!.setDisplayHomeAsUpEnabled(true) binding.appCompatButton.setOnClickListener { - var desc = findViewById(binding.radioGroup.checkedRadioButtonId).text + var desc = when (binding.radioGroup.checkedRadioButtonId) { + R.id.radioButton2 -> getString(R.string.is_at_a_different_place_wikidata, place) + else -> findViewById(binding.radioGroup.checkedRadioButtonId).text + } var det = binding.detailsEditText.text.toString() if (binding.radioGroup.checkedRadioButtonId == R.id.radioButton3 && binding.detailsEditText.text.isNullOrEmpty()) { Toast @@ -103,4 +106,4 @@ class WikidataFeedback : BaseActivity() { onBackPressed() return true } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt index 25baf3a92..7445a6526 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.kt @@ -1064,7 +1064,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(), override fun updateListFragment(placeList: List) { adapter!!.clear() - adapter!!.items = placeList + adapter!!.items = placeList.filter{ it.name.isNotEmpty() } binding!!.bottomSheetNearby.noResultsMessage.visibility = if (placeList.isEmpty()) View.VISIBLE else View.GONE } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt index f28cc833e..c39d8901d 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt +++ b/app/src/main/java/fr/free/nrw/commons/nearby/model/NearbyResultItem.kt @@ -18,6 +18,8 @@ class NearbyResultItem( @field:SerializedName("description") private val description: ResultTuple?, @field:SerializedName("endTime") private val endTime: ResultTuple?, @field:SerializedName("monument") private val monument: ResultTuple?, + @field:SerializedName("dateOfOfficialClosure") private val dateOfOfficialClosure: ResultTuple?, + @field:SerializedName("pointInTime") private val pointInTime: ResultTuple?, ) { fun getItem(): ResultTuple = item ?: ResultTuple() @@ -41,6 +43,8 @@ class NearbyResultItem( fun getDestroyed(): ResultTuple = destroyed ?: ResultTuple() + fun getDateOfOfficialClosure(): ResultTuple = dateOfOfficialClosure ?: ResultTuple() + fun getDescription(): ResultTuple = description ?: ResultTuple() fun getEndTime(): ResultTuple = endTime ?: ResultTuple() @@ -48,4 +52,7 @@ class NearbyResultItem( fun getAddress(): String = address?.value ?: "" fun getMonument(): ResultTuple? = monument + + fun getPointInTime(): ResultTuple = pointInTime ?: ResultTuple() + } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt index a870db86b..38e7dace8 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.kt @@ -14,6 +14,7 @@ import android.os.Bundle import android.provider.Settings import android.view.View import android.widget.CheckBox +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -122,7 +123,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C /** * Set the value of the showPermissionDialog variable. * - * @param showPermissionsDialog `true` to indicate to show + * @property isShowPermissionsDialog `true` to indicate to show * Permissions Dialog if permissions are missing, `false` otherwise. */ /** @@ -166,6 +167,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C private var _binding: ActivityUploadBinding? = null private val binding: ActivityUploadBinding get() = _binding!! + private lateinit var onBackPressedCallback: OnBackPressedCallback + @SuppressLint("CheckResult") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -176,6 +179,23 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C _binding = ActivityUploadBinding.inflate(layoutInflater) setContentView(binding.root) + // Overrides the back button to make sure the user is prepared to lose their progress + onBackPressedCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + showAlertDialog( + this@UploadActivity, + 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() + } + } + } + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + /* If Configuration of device is changed then get the new fragments created by the system and populate the fragments ArrayList @@ -190,7 +210,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } init() - binding.rlContainerTitle.setOnClickListener { v: View? -> onRlContainerTitleClicked() } + binding.rlContainerTitle.setOnClickListener { _: View? -> onRlContainerTitleClicked() } nearbyPopupAnswers = mutableMapOf() //getting the current dpi of the device and if it is less than 320dp i.e. overlapping //threshold, thumbnails automatically minimizes @@ -204,7 +224,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } locationManager!!.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) locationManager!!.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) - store = BasicKvStore(this, storeNameForCurrentUploadImagesSize).apply { + store = BasicKvStore(this, STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE).apply { clearAll() } checkStoragePermissions() @@ -244,7 +264,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onPageSelected(position: Int) { currentSelectedPosition = position - if (position >= uploadableFiles!!.size) { + if (position >= uploadableFiles.size) { binding.cvContainerTopCard.visibility = View.GONE } else { thumbnailsAdapter!!.notifyDataSetChanged() @@ -277,7 +297,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .filter { result: Boolean? -> result!! } - .subscribe { result: Boolean? -> + .subscribe { _: Boolean? -> showAlertDialog( this, getString(R.string.block_notification_title), @@ -287,7 +307,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C }) } - fun checkStoragePermissions() { + private fun checkStoragePermissions() { // Check if all required permissions are granted val hasAllPermissions = hasPermission(this, PERMISSIONS_STORAGE) val hasPartialAccess = hasPartialAccess(this) @@ -358,7 +378,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C showLongToast(this, messageResourceId) } - override fun getUploadableFiles(): List? { + override fun getUploadableFiles(): List { return uploadableFiles } @@ -370,6 +390,14 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onUploadMediaDeleted(index: Int) { fragments!!.removeAt(index) //Remove the corresponding fragment uploadableFiles.removeAt(index) //Remove the files from the list + + val isMediaDetailFragment = fragments!!.getOrNull(currentSelectedPosition)?.let { + it is UploadMediaDetailFragment + } ?: false + if(!isMediaDetailFragment) { + // Should hide the top card current fragment is not the media detail fragment + showHideTopCard(false) + } thumbnailsAdapter!!.notifyItemRemoved(index) //Notify the thumbnails adapter uploadImagesAdapter!!.notifyDataSetChanged() //Notify the ViewPager } @@ -378,8 +406,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C binding.tvTopCardTitle.text = resources .getQuantityString( R.plurals.upload_count_title, - uploadableFiles!!.size, - uploadableFiles!!.size + uploadableFiles.size, + uploadableFiles.size ) } @@ -447,15 +475,16 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C receiveInternalSharedItems() } - if (uploadableFiles == null || uploadableFiles!!.isEmpty()) { + if (uploadableFiles.isEmpty()) { handleNullMedia() } else { //Show thumbnails - if (uploadableFiles!!.size > 1) { - if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { //If there is only file, no need to show the image thumbnails + if (uploadableFiles.size > 1) { + if (!defaultKvStore.getBoolean("hasAlreadyLaunchedCategoriesDialog")) { + // If there is only file, no need to show the image thumbnails showAlertDialogForCategories() } - if (uploadableFiles!!.size > 3 && + if (uploadableFiles.size > 3 && !defaultKvStore.getBoolean("hasAlreadyLaunchedBigMultiupload") ) { showAlertForBattery() @@ -467,8 +496,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C binding.tvTopCardTitle.text = resources .getQuantityString( R.plurals.upload_count_title, - uploadableFiles!!.size, - uploadableFiles!!.size + uploadableFiles.size, + uploadableFiles.size ) @@ -477,7 +506,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } - for (uploadableFile in uploadableFiles!!) { + for (uploadableFile in uploadableFiles) { val uploadMediaDetailFragment = UploadMediaDetailFragment() if (!uploadIsOfAPlace) { @@ -500,8 +529,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C object : UploadMediaDetailFragmentCallback { override fun deletePictureAtIndex(index: Int) { store!!.putInt( - keyForCurrentUploadImagesSize, - (store!!.getInt(keyForCurrentUploadImagesSize) - 1) + KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE, + (store!!.getInt(KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE) - 1) ) presenter!!.deletePictureAtIndex(index) } @@ -579,11 +608,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C fragments!!.add(mediaLicenseFragment!!) } else { for (i in 1 until fragments!!.size) { - fragments!![i]!!.callback = object : UploadBaseFragment.Callback { + fragments!![i].callback = object : UploadBaseFragment.Callback { override fun onNextButtonClicked(index: Int) { if (index < fragments!!.size - 1) { binding.vpUpload.setCurrentItem(index + 1, false) - fragments!![index + 1]!!.onBecameVisible() + fragments!![index + 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset( if ((index > 0)) index - 1 else 0, @@ -597,7 +626,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onPreviousButtonClicked(index: Int) { if (index != 0) { binding.vpUpload.setCurrentItem(index - 1, true) - fragments!![index - 1]!!.onBecameVisible() + fragments!![index - 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset( if ((index > 3)) index - 2 else 0, @@ -635,11 +664,12 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C binding.vpUpload.offscreenPageLimit = fragments!!.size } // Saving size of uploadableFiles - store!!.putInt(keyForCurrentUploadImagesSize, uploadableFiles!!.size) + store!!.putInt(KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE, uploadableFiles.size) } /** - * Changes current image when one image upload is cancelled, to highlight next image in the top thumbnail. + * Changes current image when one image upload is cancelled, to highlight next image in the top + * thumbnail. * Fixes: [Issue](https://github.com/commons-app/apps-android-commons/issues/5511) * * @param index Index of image to be removed @@ -774,7 +804,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onNextButtonClicked(index: Int) { if (index < fragments!!.size - 1) { binding.vpUpload.setCurrentItem(index + 1, false) - fragments!![index + 1]!!.onBecameVisible() + fragments!![index + 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset(if ((index > 0)) index - 1 else 0, 0) if (index < fragments!!.size - 4) { @@ -789,10 +819,10 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C override fun onPreviousButtonClicked(index: Int) { if (index != 0) { binding.vpUpload.setCurrentItem(index - 1, true) - fragments!![index - 1]!!.onBecameVisible() + fragments!![index - 1].onBecameVisible() (binding.rvThumbnails.layoutManager as LinearLayoutManager) .scrollToPositionWithOffset(if ((index > 3)) index - 2 else 0, 0) - if ((index != 1) && ((index - 1) < uploadableFiles!!.size)) { + if ((index != 1) && ((index - 1) < uploadableFiles.size)) { // Shows the top card if it was hidden because of the last image being deleted and // now the user has hit previous button to go back to the media details showHideTopCard(true) @@ -800,7 +830,10 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } } - override fun onThumbnailDeleted(position: Int) = presenter!!.deletePictureAtIndex(position) + override fun onThumbnailDeleted(position: Int) { + presenter!!.deletePictureAtIndex(position) + thumbnailsAdapter?.notifyDataSetChanged() + } /** * The adapter used to show image upload intermediate fragments @@ -827,11 +860,11 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C } - fun onRlContainerTitleClicked() { + private fun onRlContainerTitleClicked() { binding.rvThumbnails.visibility = if (isTitleExpanded) View.GONE else View.VISIBLE isTitleExpanded = !isTitleExpanded - binding.ibToggleTopCard.rotation = binding.ibToggleTopCard.rotation + 180 + binding.ibToggleTopCard.rotation += 180 } override fun onDestroy() { @@ -848,21 +881,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C if (uploadCategoriesFragment != null) { uploadCategoriesFragment!!.callback = null } - } - - /** - * 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() } + onBackPressedCallback.remove() } /** @@ -882,7 +901,7 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C .setView(view) .setTitle(getString(R.string.multiple_files_depiction_header)) .setMessage(getString(R.string.multiple_files_depiction)) - .setPositiveButton("OK") { dialog: DialogInterface?, which: Int -> + .setPositiveButton("OK") { _: DialogInterface?, _: Int -> if (checkBox.isChecked) { // Save the user's choice to not show the dialog again defaultKvStore.putBoolean("hasAlreadyLaunchedCategoriesDialog", true) @@ -915,14 +934,14 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C getString(R.string.cancel), { /* Since opening the right settings page might be device dependent, using - https://github.com/WaseemSabir/BatteryPermissionHelper - directly appeared like a promising idea. - However, this simply closed the popup and did not make - the settings page appear on a Pixel as well as a Xiaomi device. - Used the standard intent instead of using this library as - it shows a list of all the apps on the device and allows users to - turn battery optimisation off. - */ + https://github.com/WaseemSabir/BatteryPermissionHelper + directly appeared like a promising idea. + However, this simply closed the popup and did not make + the settings page appear on a Pixel as well as a Xiaomi device. + Used the standard intent instead of using this library as + it shows a list of all the apps on the device and allows users to + turn battery optimisation off. + */ val batteryOptimisationSettingsIntent = Intent( Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS ) @@ -960,7 +979,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C Also, location information is discarded if the difference between current location and location recorded just before capturing the image is greater than 100 meters */ - if (isLocationTagUnchecked || locationDifference > 100 || !defaultKvStore.getBoolean("inAppCameraLocationPref") + if (isLocationTagUnchecked || locationDifference > 100 + || !defaultKvStore.getBoolean("inAppCameraLocationPref") || !isInAppCameraUpload ) { currLocation = null @@ -981,8 +1001,8 @@ class UploadActivity : BaseActivity(), UploadContract.View, UploadBaseFragment.C @JvmField var nearbyPopupAnswers: MutableMap? = null - const val keyForCurrentUploadImagesSize: String = "CurrentUploadImagesSize" - const val storeNameForCurrentUploadImagesSize: String = "CurrentUploadImageQualities" + const val KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE: String = "CurrentUploadImagesSize" + const val STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE: String = "CurrentUploadImageQualities" /** * Sets the flag indicating whether the upload is of a specific place. diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt index 32f32fff8..cf3816682 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadPresenter.kt @@ -159,34 +159,31 @@ class UploadPresenter @Inject internal constructor( override fun deletePictureAtIndex(index: Int) { val uploadableFiles = view.getUploadableFiles() - if (index == uploadableFiles!!.size - 1) { - // If the next fragment to be shown is not one of the MediaDetailsFragment - // lets hide the top card so that it doesn't appear on the other fragments - view.showHideTopCard(false) - } - view.setImageCancelled(true) - repository.deletePicture(uploadableFiles[index].getFilePath()) - if (uploadableFiles.size == 1) { - view.showMessage(R.string.upload_cancelled) - view.finish() - return - } - - presenter.updateImageQualitiesJSON(uploadableFiles.size, index) - view.onUploadMediaDeleted(index) - if (index != uploadableFiles.size && index != 0) { - // if the deleted image was not the last item to be uploaded, check quality of next - repository.getUploadItem(index)?.let { - presenter.checkImageQuality(it, index) + uploadableFiles?.let { + view.setImageCancelled(true) + repository.deletePicture(uploadableFiles[index].getFilePath()) + if (uploadableFiles.size == 1) { + view.showMessage(R.string.upload_cancelled) + view.finish() + return } - } - if (uploadableFiles.size < 2) { - view.showHideTopCard(false) - } + presenter.updateImageQualitiesJSON(uploadableFiles.size, index) + view.onUploadMediaDeleted(index) + if (index != uploadableFiles.size && index != 0) { + // if the deleted image was not the last item to be uploaded, check quality of next + repository.getUploadItem(index)?.let { + presenter.checkImageQuality(it, index) + } + } - //In case lets update the number of uploadable media - view.updateTopCardTitle() + if (uploadableFiles.size < 2) { + view.showHideTopCard(false) + } + + //In case lets update the number of uploadable media + view.updateTopCardTitle() + } } override fun onAttachView(view: UploadContract.View) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt index 7a92cf6c5..262013045 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.kt @@ -97,9 +97,9 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { } if (media == null) { if (callback != null) { - binding!!.tvTitle.text = getString(R.string.step_count, - callback!!.getIndexInViewFlipper(this) + 1, - callback!!.totalNumberOfSteps, + binding?.tvTitle?.text = getString(R.string.step_count, + callback?.getIndexInViewFlipper(this)?.plus(1) ?: 1, + callback?.totalNumberOfSteps ?: 1, getString(R.string.categories_activity_title)) } } else { @@ -110,7 +110,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { } setTvSubTitle() - binding!!.tooltip.setOnClickListener { + binding?.let { it.tooltip.setOnClickListener { showAlertDialog( requireActivity(), getString(R.string.categories_activity_title), @@ -119,10 +119,11 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { null ) } + } if (media == null) { - presenter!!.onAttachView(this) + presenter?.onAttachView(this) } else { - presenter!!.onAttachViewWithMedia(this, media!!) + presenter?.onAttachViewWithMedia(this, media!!) } binding!!.btnNext.setOnClickListener { v: View? -> onNextButtonClicked() } binding!!.btnPrevious.setOnClickListener { v: View? -> onPreviousButtonClicked() } @@ -137,7 +138,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { } subscribe = RxTextView.textChanges(binding!!.etSearch) .doOnEach { v: Notification? -> - binding!!.tilContainerSearch.error = + binding?.tilContainerSearch?.error = null } .takeUntil(RxView.detaches(binding!!.etSearch)) @@ -163,25 +164,25 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { } private fun searchForCategory(query: String) { - presenter!!.searchForCategories(query) + presenter?.searchForCategories(query) } private fun initRecyclerView() { - adapter = UploadCategoryAdapter({ categoryItem: CategoryItem? -> - presenter!!.onCategoryItemClicked(categoryItem!!) + if (adapter == null) { adapter = UploadCategoryAdapter({ categoryItem: CategoryItem? -> + presenter?.onCategoryItemClicked(categoryItem!!) Unit }, nearbyPlaceCategory) - - if (binding != null) { - binding!!.rvCategories.layoutManager = LinearLayoutManager(context) - binding!!.rvCategories.adapter = adapter + } + binding?.rvCategories?.apply { + layoutManager = LinearLayoutManager(context) + adapter = this@UploadCategoriesFragment.adapter } } override fun onDestroyView() { super.onDestroyView() - presenter!!.onDetachView() - subscribe!!.dispose() + presenter?.onDetachView() + subscribe?.dispose() } override fun showProgress(shouldShow: Boolean) { @@ -197,6 +198,11 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { } override fun setCategories(categories: List?) { + if (adapter == null) { + Timber.e("Adapter is null in setCategories") + return + } + if (categories == null) { adapter!!.clear() } else { @@ -204,19 +210,16 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { } adapter!!.notifyDataSetChanged() - if (binding == null) { - return - } - // Nested waiting for search result data to load into the category - // list and smoothly scroll to the top of the search result list. - binding!!.rvCategories.post { - binding!!.rvCategories.smoothScrollToPosition(0) - binding!!.rvCategories.post { - binding!!.rvCategories.smoothScrollToPosition( + binding?.let { + it.rvCategories.post { + it.rvCategories.smoothScrollToPosition(0) + it.rvCategories.post { + it.rvCategories.smoothScrollToPosition( 0 ) + } } - } + } ?: Timber.e("Binding is null in setCategories") } override fun goToNextScreen() { @@ -308,7 +311,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { fun onNextButtonClicked() { if (media != null) { - presenter!!.updateCategories(media!!, wikiText!!) + presenter?.updateCategories(media!!, wikiText!!) } else { presenter!!.verifyCategories() } @@ -318,7 +321,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { if (media != null) { presenter!!.clearPreviousSelection() adapter!!.items = null - val mediaDetailFragment = checkNotNull(parentFragment as MediaDetailFragment?) + val mediaDetailFragment = parentFragment as? MediaDetailFragment?: return mediaDetailFragment.onResume() goBackToPreviousScreen() } else { @@ -345,7 +348,7 @@ class UploadCategoriesFragment : UploadBaseFragment(), CategoriesContract.View { super.onResume() if (media != null) { - binding!!.etSearch.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? -> + binding?.etSearch?.setOnKeyListener { v: View?, keyCode: Int, event: KeyEvent? -> if (keyCode == KeyEvent.KEYCODE_BACK) { binding!!.etSearch.clearFocus() presenter!!.clearPreviousSelection() diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt index af850a7e3..4a4c13ba7 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.kt @@ -532,7 +532,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra basicKvStore!!.putBoolean(keyForShowingAlertDialog, false) if (isInternetConnectionEstablished(requireActivity())) { val sizeOfUploads = basicKvStore!!.getInt( - UploadActivity.keyForCurrentUploadImagesSize + UploadActivity.KEY_FOR_CURRENT_UPLOAD_IMAGE_SIZE ) for (i in indexOfFragment until sizeOfUploads) { presenter.getImageQuality( diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt index 90c426091..77999cf2f 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.kt @@ -310,7 +310,7 @@ class UploadMediaPresenter @Inject constructor( private fun storeImageQuality( imageResult: Int, uploadItemIndex: Int, activity: Activity, uploadItem: UploadItem ) { - val store = BasicKvStore(activity, UploadActivity.storeNameForCurrentUploadImagesSize) + val store = BasicKvStore(activity, UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) val value = store.getString(UPLOAD_QUALITIES_KEY, null) try { val jsonObject = value.asJsonObject().apply { @@ -339,8 +339,10 @@ class UploadMediaPresenter @Inject constructor( */ override fun checkImageQuality(uploadItem: UploadItem, index: Int) { if ((uploadItem.imageQuality != IMAGE_OK) && (uploadItem.imageQuality != IMAGE_KEEP)) { - val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) } + + val value = basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) } ?.getString(UPLOAD_QUALITIES_KEY, null) + try { val imageQuality = value.asJsonObject()["UploadItem$index"] as Int view.showProgress(false) @@ -363,8 +365,9 @@ class UploadMediaPresenter @Inject constructor( * @param index Index of the UploadItem which was deleted */ override fun updateImageQualitiesJSON(size: Int, index: Int) { - val value = basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) } + val value = basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) } ?.getString(UPLOAD_QUALITIES_KEY, null) + try { val jsonObject = value.asJsonObject().apply { for (i in index until (size - 1)) { @@ -372,7 +375,8 @@ class UploadMediaPresenter @Inject constructor( } remove("UploadItem" + (size - 1)) } - basicKvStoreFactory?.let { it(UploadActivity.storeNameForCurrentUploadImagesSize) } + + basicKvStoreFactory?.let { it(UploadActivity.STORE_NAME_FOR_CURRENT_UPLOAD_IMAGE_SIZE) } ?.putString(UPLOAD_QUALITIES_KEY, jsonObject.toString()) } catch (e: Exception) { Timber.e(e) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaAttributionUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaAttributionUtil.kt new file mode 100644 index 000000000..7a66a87de --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaAttributionUtil.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.icu.text.ListFormatter +import android.os.Build +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.media.IdAndLabels +import java.util.Locale + +object MediaAttributionUtil { + fun getTagLine(media: Media, context: Context): String { + val uploader = media.user + val author = media.getAttributedAuthor() + return if (author.isNullOrEmpty()) { + context.getString(R.string.image_uploaded_by, uploader) + } else if (author == uploader) { + context.getString(R.string.image_tag_line_created_and_uploaded_by, author) + } else { + context.getString( + R.string.image_tag_line_created_by_and_uploaded_by, + author, + uploader + ) + } + } + + fun getCreatorName(idAndLabels: List): String? { + val locale = Locale.getDefault() + val names = idAndLabels.map{ x -> x.getLocalizedLabel(locale.language)} + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val formatter = ListFormatter.getInstance(locale) + return formatter.format(names) + } else { + return names.joinToString(", ") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt index b3c58d8b2..59c6aea97 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt @@ -1,9 +1,8 @@ package fr.free.nrw.commons.utils -import android.os.Build -import android.text.Html import android.text.Spanned import android.text.SpannedString +import androidx.core.text.HtmlCompat object StringUtil { @@ -26,12 +25,6 @@ object StringUtil { .replace("‏", "\u200F") .replace("&", "&") - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - Html.fromHtml(processedSource, Html.FROM_HTML_MODE_LEGACY) - } else { - //noinspection deprecation - @Suppress("DEPRECATION") - Html.fromHtml(processedSource) - } + return HtmlCompat.fromHtml(processedSource, HtmlCompat.FROM_HTML_MODE_LEGACY) } } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt index 5e82c3c80..c4b95d0c5 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataProperties.kt @@ -7,6 +7,7 @@ enum class WikidataProperties( ) { IMAGE("P18"), DEPICTS(BuildConfig.DEPICTS_PROPERTY), + CREATOR(BuildConfig.CREATOR_PROPERTY), COMMONS_CATEGORY("P373"), INSTANCE_OF("P31"), MEDIA_LEGENDS("P2096"), diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt index 63c018252..53fc44bc9 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ExtMetadata.kt @@ -1,7 +1,6 @@ package fr.free.nrw.commons.wikidata.model.gallery import com.google.gson.annotations.SerializedName -import org.apache.commons.lang3.StringUtils class ExtMetadata { @SerializedName("DateTime") private val dateTime: Values? = null diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt index 492e2e1f8..7645e438d 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/gallery/ImageInfo.kt @@ -72,7 +72,6 @@ open class ImageInfo : Serializable { } fun getThumbUrl(): String { - updateThumbUrl() return thumbUrl ?: "" } diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 7cfd761a7..0da9f5d9f 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -146,6 +146,7 @@ android:layout_marginEnd="@dimen/standard_gap" android:layout_marginRight="@dimen/standard_gap" android:layout_marginBottom="@dimen/standard_gap" + android:hint="@string/_2fa_code" android:visibility="gone" app:passwordToggleEnabled="false" tools:visibility="visible"> @@ -154,9 +155,7 @@ android:id="@+id/login_two_factor" android:layout_width="match_parent" android:layout_height="wrap_content" - android:hint="@string/_2fa_code" android:imeOptions="flagNoExtractUi" - android:inputType="number" android:visibility="gone" tools:visibility="visible" /> diff --git a/app/src/main/res/layout/activity_wikidata_feedback.xml b/app/src/main/res/layout/activity_wikidata_feedback.xml index 5f6647efe..52034b3ac 100644 --- a/app/src/main/res/layout/activity_wikidata_feedback.xml +++ b/app/src/main/res/layout/activity_wikidata_feedback.xml @@ -102,6 +102,7 @@ android:layout_marginEnd="8dp" android:text="SEND" android:visibility="visible" + android:textColor="@color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@+id/textHeader" app:layout_constraintHorizontal_bias="1.0" diff --git a/app/src/main/res/layout/fragment_explore_map.xml b/app/src/main/res/layout/fragment_explore_map.xml index e2d628dab..ad0b40b82 100644 --- a/app/src/main/res/layout/fragment_explore_map.xml +++ b/app/src/main/res/layout/fragment_explore_map.xml @@ -26,10 +26,11 @@ android:clickable="true" android:focusable="true" android:visibility="visible" - app:backgroundTint="@color/main_background_light" + app:backgroundTint="?attr/colorSurface" app:elevation="@dimen/dimen_6" app:fabSize="normal" app:srcCompat="@drawable/ic_my_location_black_24dp" + app:tint="?attr/colorOnSurface" app:useCompatPadding="true" /> الكثير من المحاولات غير الناجحة. الرجاء المحاولة مرة أخرى في بضع دقائق. عذراً، لقد تم منع هذا المستخدم على كومنز يجب توفير رمز التحقق المزدوج. + تم إرسال رمز التحقق إلى بريدك الإلكتروني. يُرجى إدخال الرمز لتسجيل الدخول. فشل تسجيل الدخول ارفع اسم هذه المجموعة @@ -273,6 +275,7 @@ انضم لمختبري اصدارات Beta (بيتا) يمكنك الاشتراك في القناة التجريبية على جوجل بلاي والحصول على إمكانية الوصول المبكر إلى الميزات الجديدة وإصلاحات الأخطاء رمز التحقق المزدوج 2FA + رمز التحقق من البريد الإلكتروني أترغب فعلا في الخروج؟ صورة الوسائط فشلت لم يتم العثور على تصنيفات فرعية. @@ -851,6 +854,7 @@ نقاش اكتب شيئًا عن العنصر \'%1$s\'. سيكون مرئيًا للعامة. \'%1$s\' لم يعد موجودًا، ولا يمكن التقاط صورة له أبدًا. + \'%1$s\' في مكان آخر. \'%1$s\' موجود في مكان مختلف. يرجى تحديد المكان الصحيح أدناه، وإذا أمكن، اكتب خط العرض وخط الطول الصحيحين. مشكلة أو معلومات أخرى (يرجى التوضيح أدناه). سيتم نشر تعليقاتك على صفحة الويكي التالية: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> @@ -887,4 +891,6 @@ مبروك، جميع الصور الموجودة في هذا الألبوم تم تحميلها أو تم وضع علامة عليها بأنها غير قابلة للتحميل. عرض في استكشاف عرض في المناطق القريبة + تم الإنشاء والتحميل بواسطة: %1$s + تم إنشاؤه بواسطة %1$s وتم تحميله بواسطة %2$s diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index a7a3da9a7..9b2fde313 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -2,6 +2,7 @@ @@ -8,6 +9,8 @@ Tangalt aɣbalu Github n Commons Alugu Commons Asmel wen n Commons + Aglam + Akk %1$d n ufaylu yezga yettali %1$d n ifuyla zgan ttalin @@ -49,6 +52,7 @@ Jerred Tuqqna... Rǧu... + Txil, rǧu… Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -85,12 +89,14 @@ Nadi taggayin Sekles Smiren + Tabdart Usla ayen yulin Ulac taggayin imenṭaḍen akked %1$s i yettwafen Rnu taggayin akken ad terreḍ tugniwin-ik sehlent i tifin di Wikimedia Commons.\n\nBdu timerna n taggayin. Taggayin Iɣewwaṛen Jerred + Taggayt Ɣef Asnas Wikipedia Commons d asnas n uɣbalu yeldin i d-yennulfan udiɣ yettwarfed sɣur iwiziwen d wid i t-iḍemnen n tmezdagnut Wikimedia. Tasbeddit Wikimedia ur tetteka ara deg usnulfu-is, tanfelit-is, neɣ aseggem n usnas. Rnu <a href=\"%1$s\">tummla tamaynut GitHub</a> akken ad temmleḍ ibugen neɣ ad d-mudded isumar. @@ -129,6 +135,7 @@ Zgel axedddim yettwaḥerzen ara tafeḍ di Internet akked tugniwin n inazalen, tiduliwin n yidlisen, atg. Tɣileḍ igarrez? Ih! + Ugar n telɣut Taggayin Asali... Ula d yiwet ur tettwafren @@ -139,10 +146,12 @@ Ysera tasiregt: Aru deg usekles azɣaray. Asnas ur yezmir ara ad yeddu s war aya. IH Ɣur-k + Sali Ih Uhu Azwel Aglam + Ameskar Azemz n usali Turagt imsidag @@ -180,6 +189,16 @@ Qqen ar umiḍan-ik Azen afaylu n uɣmis Azen afaylu n uɣmis i yinermisen s yimayl + Qqen Ulac adeg Ilaq usireg i uskan tabdart n wadigen iqerben + Wikidata + Wikipedia + Isteqsiyen FAQ + Suqel + Tutlayin + Semmet + Nadi + Nadi + Taggayin diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 1e482148e..0d368b91d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -16,6 +16,7 @@ * Revi * Suleiman the Magnificent Television * Theshinster123 +* YeBoy371 * Ykhwong * YuzaTea * 그냥기여자 @@ -226,10 +227,11 @@ 베타 테스터가 되세요 구글 플레이 베타 채널에서 메일 발송을 허가해서 새로운 기능이나 버그 수정에 대한 정보를 빨리 얻어보세요 2FA 코드 + 이메일 인증 코드 정말 로그아웃하시겠습니까? 미디어 그림 실패 하위 분류가 없습니다 - 부모 분류가 없습니다 + 상위 분류가 없습니다 자오 연봉 라마 레인보우 브리지 @@ -428,7 +430,7 @@ 위치 권한 없이 사진이 필요한 주변 장소를 표시할 수 없습니다 다시는 묻지 않음 위치 권한 요청 - 무언가가 잘못되어, 저희는 당신의 업적을 가져올 수 없었습니다 + 오류가 발생하여, 업적을 가져올 수 없습니다. 캠페인 표시 진행되고 있는 캠페인 보기 허용 @@ -449,6 +451,7 @@ 감사 표현 보내기: 실패 이것이 저작권 규정을 준수하고 있습니까? 알맞게 분류됐습니까? + 이것이 범위에 포함되나요? 기여자에게 감사를 표하시겠습니까? 앗, 분류가 달리지 않은 것 같습니다! 이 이미지는 %1$s 분류에 속해 있습니다. @@ -486,6 +489,7 @@ 분류가 없습니다 서술이 발견되지 않았습니다 업로드 취소됨 + %1$s을(를) 삭제해야 하는 이유는 무엇인가요? 기본 설명 언어 삭제 신청 성공 @@ -530,7 +534,7 @@ 로그인에 문제가 생겼습니다, 비밀번호를 재설정해야 합니다! 미디어 자식 클래스 - 부모 클래스 + 상위 클래스 주변 장소 발견 %1$s의 사진이 맞습니까? %1$s의 사진이 맞습니까? @@ -688,6 +692,7 @@ 의견을 입력해 주십시오 토론 기타 문제 또는 정보 (아래에 설명해 주십시오) + 모든 업로드를 취소하시겠습니까? 모든 업로드를 취소하는 중... 업로드 보류 중 diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index bf4f31998..81d8842ff 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -101,6 +101,8 @@ Суратха ал Джуўукъда Джюклегенлерим + Джибериуню копия эт + Джибериу алмашдырыу буферге копия этилгенди Юлюшле Файлны бетине къара Тюб джазыу (Амалсыз) @@ -274,6 +276,7 @@ Викитекстни алмашдырыу буферге копия эт Викитекст алмашдырыу буферге копия этилди Джууукъдагыла тюз ишлеялмайды, Локация хайырландырылалмайды. + Интернет джетишмейди. Къуру кэш этилген джерле кёргюзюледиле. Локациягъа джетишиу уналмады. Бу функцияны хайырланыр ючюн, тилейбиз, локациягъызны къолугъуз бла белгилегиз. Джууукъдагъы джерле тизмени кёргюзюр ючююн, эркинлик берирге керекди Джууукъдагъы суратла тизмени кёргюзюр ючююн, эркинлик берирге керекди @@ -357,11 +360,13 @@ Кетер Джетишимле Профиль + Белгичикле Статистика Бюсюреуле Алындыла Сайланнган Суратла \"Джууукъдагъы Джерле\" юсю бла суратла - Дараджа + Дараджа %d + %s (Дараджа %s) Суратла Джюклендиле Суратла Кери Алынмадыла Суратла Хайырландыла @@ -393,6 +398,7 @@ Девайсыгъызда келишген картография къошакъ табылмады. Тилейбиз, бу энчиликни хайырландырыр ючюн картография къошакъ джюклегиз. Суратла Локацияла + Категорияла Китаб белгилени къош/къорат Китаб белгиле Алкъын чырт китаб белги къошмадыгъыз @@ -794,4 +800,19 @@ Бу джерни сураты джокъду, хайда бирин эт! Бу джерни алайсыз да сураты барды. Бу джерни сураты болуб-болмагъанын тинте турама. + Джюкленнген заманда халат + Бир хайырланыучу да табылмады + Гёзен + Башха викиле + Файлны хайырланыулары + SingleWebViewActivity + Хыйсаб + Хыйсабны сюрт + Хыйсаб сюртюуню эсгертиую + Джокъ этиу — <b>ахыр амалды</b>, эмда аны <b>тюзетиуню тамамы бла тохтатыргъа излегесиз хайырланыргъа керекди</b>, неда эскиде ассоциацияланы бир мадар болуб аслам джашырыргъа излесегиз.<br/><br />Викигёзенде хыйсабны кетериу, башхала хыйсабны джокъ этиу атны джюрютген процессде сизни кошумугъузну танымазча, хыйсабыгъызны атын тюрлендириу бла этиледи.<b>Джокъ этиу толу анонимликни гарантия этмейди эмда проектде къошумларыгъызны къоратмайды</b>. + Тюб джазыу + Тюб джазыу алмашдырыу буферге копия этилгенди + Алгъышлайбыз, бу альбомна бютеу сратла не джюкленнгендиле, неда джюкленирге джораланмагъанлача белгиленнгендиле. + Explore-де кёргюз + Nearby-да кёргюз diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 2dc2fb995..54364b533 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -179,7 +179,7 @@ Spasî Hate Wergirtin Wêneyên Bijartî Wêneyên bi riya \"Cihên Nêz\" - Derece + Derece %d Wêneyên Barkirî Wêneyê din Belê, çima na diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 52a1243aa..17cb550ae 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -1,6 +1,7 @@ + د خونديځ فيسبوک پاڼه خونديځ ګيټهوب سرچينه کوډ خونديځ نښان خونديځ وېبپاڼه @@ -19,9 +20,9 @@ د کامرې له لارې ونډه ورزياتول انځورونو له لارې ونډه ورزياتول د پخوانيو ونډو له انځورتونه د ونډې ورزياتول - نيونګې + نيونگې ژبې سپيناوی - نيونګ + نيونگ سپيناوی انځور ټول @@ -46,35 +47,55 @@ %d upload %d پورته کول - دا انځور به د %1$s په منښتليک سمبال وي. + + دا انځور به د منښتليک %1$s لاندې وي + دا انځورونه به د %1$s منښتليک لاندې وي + + + %1$d راپورته کول + %1$d راپورته کېدنې + + + وېشل شوې منځپانگه ترلاسه کوي.د انځورونو بهير شايد يو څه وخت ونيسي، دا د انځورونو په کچې او ستاسو وسيلې پورې اړه لري + وېشل شوې منځپانگه ترلاسه کوي.د انځورونو بهير شايد يو څه وخت ونيسي، دا د انځورونو په کچې او ستاسو وسيلې پورې اړه لري + سپړنه ښکارېدنه - ټولګړی - غبرګون + ټولگړی + غبرگون پټنتيا - ويکي خونديځ - امستنې + ويکي‌خونديځ + اوڼنې خونديځ ته راپورته کول راپورته کول جريان لري کارن‌نوم پټنوم - خپل خونديځ بېټا ګڼون ته ورننوځئ + خپل خونديځ بېټا گڼون ته ورننوځئ ننوتل پټنوم مو هېر شوی؟ نومليکنه په ننوتلو کې دی - لطفاً تم شۍ … - نيونګې او سپيناوي تازه کول - په تمه اوسئ - بريالی ننوتون - ناسم ننوتون - دوتنه و نه موندل شوه. لطفاً د يوې بلې دوتنې د موندلو هڅه وکړئ. - د بياځلي هڅې وروستۍ اندازه پوره شوه! مهرباني وکړئ، لغوه يې کړئ او بيا د راپورته کولو هڅه وکړئ + په تمه اوسئ... + نيونگې او سپيناوي تازه کول + په تمه اوسئ... + بريالی ننوتون! + ناسم ننوتون! + دوتنه و نه موندل شوه. مهرباني وکړئ د يوې بلې دوتنې د موندلو هڅه وکړئ. + د بياځلي هڅې وروستۍ اندازه پوره شوه! مهرباني وکړئ، ناگاره يې کړئ او بيا د راپورته کولو هڅه وکړئ بيټري سمون بندول؟ - کله چې د بیټرۍ اصلاح بنده وي، له ۳ څخه زیاتو عکسونو اپلوډ کول ډیر باوري کار کوي. مهرباني وکړئ د اسانه اپلوډ تجربې لپاره د کامنز ایپ لپاره د ترتیباتو څخه د بیټرۍ اصلاح بند کړئ. \n\n د بیټرۍ اصلاح بندولو لپاره ممکنه ګامونه:\n\n لومړی ګام: لاندې \'ترتیبات\' تڼۍ باندې کلیک وکړئ.\n\n دوهم ګام: له \'نه غوره شوی\' څخه \'ټول ایپس\' ته واړوئ.\n\n دریم ګام: د \"کامن\" یا \"fr.free.nrw.commons\" لټون وکړئ.\n\n څلورم ګام: دا کلیک کړئ او \'غوره نه کړئ\' غوره کړئ.\n\n پنځم ګام: \'بشپړ شوی\' فشار ورکړئ. + کله چې د بيټرۍ سمون بند وي، له ۳ څخه زیاتو انځورونو راپورته کول ډېر باوري کار کوي. مهرباني وکړئ د اسانه راپورته کولو تجربې لپاره د خونديځ کارل د اوڼنو څخه بيټرۍ سمون بند کړئ.\n د بيټرۍ سمون بندولو لپاره شوني گامونه:\n\nلومړی گام: لاندې د \'اوڼنې\' تڼۍ کېکاږئ.\n\nدوهم گام: د \'ناسمون\' پرځای \'ټول کاريالونه\' وټاکئ.\n\nدرېم گام: \"خونديځ\" يا \"fr.free.nrw.commons\" وپلټئ.\nڅلورم گام: پرانيزئ او \'ناسمون\' وټاکئ.\nپينځم گام: \'وشو\'کېکاږئ. + کره توب نابريالی شو. مهرباني وکړئ، بياځلي ننوځئ. پورته کېدنه پيل شوه! + راپورته کول په کتار کې دي(د ټاکلي اړيکې ونگه چارن شوې) %1$s پورته شوی! + د خپلې راپورکېدنې کتلو لپاره دلته وټاپئ. + دوتنه راپورته کېږي: %s د %1$s پورته کول + %1$s راپورته کېدنه بشپړېږي + %1$s راپورته کېدنه نابريالۍ شوه + د %1$s راپورته کېدنه ځنډول شوې + کتلو لپاره دلته وټاپئ + کتلو لپاره دلته وټاپئ زما تازه پورته کېدنې لږ نابريال شو @@ -84,40 +105,94 @@ انځور اخيستل نژدې زما پورته کېدنې + وېبتړ لمېسل + وېبتړ ټينگدړې ته لمېسل شوی دی شريکول - سرليک + د دوتنې مخ کتل + نيونگ (اړين دی) + مهرباني وکړئ، د دې دوتنې لپاره نيونگ ورکړئ څرگندونه - د ننوتلو توان نلري - د شبکې ناکامي + نيونگ + غونډال ته ننوتنه ناشونې ده - د جال پاتې راتلنه ډیری ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ. بخښنه غواړو، په دي کارن د کامنز لخوا بنديز ولګول شو - غونډال کې ننوتنه نابريالې شوه + تاسو بايد خپل دوه لامليز تاييد کوډ ورکړئ. + ستاسو برېښليک پتې ته د ننوتلو تاييد کوډ لېږل شوی دی. مهرباني وکړئ د ننوتلو لپاره کوډ ورکړئ. + غونډال کې ننوتنه نابريالۍ شوه پورته کول + د دې ټولگې نوم بدلونونه پورته کول وېشنيزې پلټل + هغه توکي وپلټئ چې ستاسو رسنۍ يې انځوروي (غر، تاج مح، او نور.) خوندي کول + څنگزنه خوښنۍ بياتازه کول + لړليک + (تراوسه هيڅ راپورته کېدنه نشته) + %1$s سره ورته هيڅ وېشنيزې ونه موندل شوې + %1$s سره هيڅ ورته ويکي‌اومتوک توکي ونه موندل شوه + %1$s هيڅ کوشنۍ ټولگې نه لري + %1$s مور ټولگې نه لري + ويکي‌اوتوک خونديځ کې د خپلو انځورونو موندلو لپاره وېشنيزې ورگډې کړئ.\nوېشنيزو ورگډولو لپاره ټاپل پيل کړئ. وېشنيزې امستنې نومليکنه + ټاکلی انځور + دوديز ټاکونکی + وېشنيزه + ملگرو بياکتنه په اړه - <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">د پټنتيا تگلاره</a> + د ويکي‌رسنۍ خونديځ کاريال يو پرانيستې سرچينې کاريال دی چې د ويکي‌رسنۍ ټولنې د بسپنه ورکوونکو او خپل‌خوښو کارنانو له خوا جوړ شوی او ساتل کېږي. د ويکي‌رسنۍ بنسټ د دې کاريال په جوړولو، پراختيا او ساتنه کې ښکېل نه دی. + د بگ راپور او وړانديزونو لپاره يوه <a href=\"%1$s\">گيټ هاب ستونزه</a> جوړه کړئ. + د پټنتيا تگلار په اړه + غبرگون لېږنه (برېښليک له لارې) + هيڅ برېښليک سرچينه نه ده ځای پرځای شوې وروستۍ کارېدلې وېشنيزې + د لومړۍ همغږۍ په تمه... تاسې تر اوسه کوم انځور نه دی پورته کړی. بياآزمويل ناگارل ښکته کول تلواليز منښتليک - د شپې بڼه + مخکنی سرليک او سپيناو وکاروئ + شاليد + ځانگړي کونگ-ورته وېشنه ۴.۰ + ځانگړي کونگ ۴.۰ + ځانگړي کونگ-ورته وېشنه ۳.۰ + ځانگړي کونگ ۳.۰ CC BY 3.0 + ستاسو انځورونه د نړۍ په گوټ گوټ کې خلکو ته زده کړه ورکوي! + مهرباني وکړئ هغه انځورونه راپورته کړئ چې په بشپړ ډول تاسو اخيستي يا جوړ کړې وي: + طبيعي څيزونه (گلان، څاروي، غرونه) + گټور څيزونه (بايسيکلونه، اورگډو تمځايونه) + نوميالي وگړي (ستاسو ښاروال، اولمپيک لوبغاړی چې تاسو ورسره وليدل) + مهرباني وکړئ مه يې راپورته کوئ: + د خپلو ملگرو ځان‌انځورنه او انځورونه + هغه انځورنه چې تاسو له اينټرنېټ څخه راکښته کړې وي + د ملکيتي کاريالونو سکرين‌شاټونه + د پورته کولو بېلگه: + سرليک: سيډني اوپېرا هاوس + څرگنداوی: د خليج له خوا د سيډني اوپېرا هاوس ليدنه + وېشنيزې: له لوېديځ څخه د سېډني اوپېرا هاوس ، د سېډني اوپېرا هاوس لرليد + خپل انځورونه شريک کړئ. د ويکيپېډيا ليکنو ته ژوند ورکولو کې مرسته وکړئ! + په ويکيپېډيا کې دا انځورونه له ويکيمېډيا خونديځ نه اخيستل شوي دي. + ستاسو انځورونه د نړۍ په گوټ گوټ کې خلکو ته زده کړه ورکوي! + له اينټرنېته د لمېس رېښتو لرونکو موادو او د پوسټرونو، کتاب پوښونو او نورو انځورنو د خپرلو څخه ډډه وکړئ. + ستا په اند ته پوه شوې؟ هو + نور مالومات وېشنيزې رابرسېرېږي... هېڅ هم نه دی ټاکل شوی + هيڅ نيونگ نشته څرگندونه نشته + هيڅ شننه نشته نامعلوم جواز تازه کول + د زېرمه کولو د پرېښولي غوښتنه کول + اړينه پرېښولی: بهرنۍ زېرمه ولولئ. کاريال ستاسو انځورتونه ته پرته له دې لاسرسی نشي موندلی. ښه گواښنه هو @@ -136,5 +211,37 @@ امستنې غبرگون وتل + تگلوري + ويکي‌اومتوک + ويکيپېډيا + خونديځ + ډ‌ځ‌پ + کارن لارښود + ښوونې پرېښودل + اينټرنېټ د لاسرسی وړ نه دی + خبرتياوو ترلاسه کولو کې تېروتنه + د بياکتنې لپاره د انځور راوړول کې تېروتنه. د بياهڅې کولو لپاره تازه کولو تڼۍ کېکاږئ. + هیڅ خبرتیاوې ونه موندل شوې + ژباړل + ژبې + هغه ژبه غوره کړئ چې تاسو یې ژباړې سپارل غواړئ + پرمخ‌ځه + ناگارل + بيا هڅه‌کول + داځای انځور ته اړتيا لري. + دا ځای لادمخه انځور لري. + دا ځای نور شتون نه لري. + هيڅ انځور و‌نه موندل شو! + د انځورونو د راپورته کولو پرمهال يوه تېروتنه رامنځته شوه. + راپورته کوونکی: %1$s + بنديز لگېدلی + په خونديځ کې له سمون کولو څخه پر تاسو بنديز لگېدلی دی + د ورځې انځور + پلټل + خونديځ پلټل + پلټل + وروستۍ پلټنې: + وروستۍ پلټل شوې پوښتنې + وروستۍ ژبې پوښتنې گڼون diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index da2de6c82..6bc192c68 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -3,6 +3,7 @@ * Anaclaudiaml * Caio1478 * Duke of Wikipädia +* EGobi * Eduardo Addad de Oliveira * Eduardoaddad * Felipe L. Ewald @@ -419,7 +420,7 @@ Favoritos Não adicionou nenhum favorito Favoritos - A recolha de registos foi iniciada. REINICIE a aplicação, execute a operação que pretende registar e prima outra vez \"Enviar ficheiro de registos\" + A coleta de registros foi iniciada. REINICIE o aplicativo, execute a operação que pretende registrar e toque em ‘Enviar arquivo de registros’ novamente Eu fiz o carregamento por engano Eu não sabia que seria publicamente visível Eu percebi que é ruim para minha privacidade @@ -696,7 +697,7 @@ WLM Essa imagem será enviada ao concurso Wiki Loves Monuments Monumentos de exibição - Estamos no mês no Wiki Loves Monuments! + Chegou o mês do Wiki Loves Monuments! SABER MAIS Wiki Loves Monuments O Wiki Loves Monuments é um concurso internacional organizado pela Wikimedia sobre fotografias de monumentos diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d25d949ae..aa287c9cb 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -170,6 +170,7 @@ Слишком много неудачных попыток. Пожалуйста, попробуйте ещё раз через несколько минут. Извините, но участник с таким именем был заблокирован на Викискладе Вы должны ввести код двухфакторной аутентификации. + Код подтверждения был отправлен на адрес вашей электронной почты. Пожалуйста, введите его для входа. Ошибка входа в систему Загрузка Введите название для этой группы файлов @@ -275,6 +276,7 @@ Стать бета-тестером Подпишитесь на наш канал бета-версии на Google Play и получите ранний доступ к новым функциям и исправлениям ошибок Код 2ФА + Код подтверждения электронной почты Вы действительно хотите выйти? Ошибка медиафайла Подкатегории не найдены. @@ -882,4 +884,6 @@ Поздравляем, все фотографии в этом альбоме либо загружены, либо помечены как не предназначенные для загрузки. Показать в Explore Показать в Nearby + Создано и загружено: %1$s + Создано %1$s и загружено %2$s diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 614f460be..d81d5ba73 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -90,13 +90,13 @@ Prosimo, počakajte ... Posodabljam napise in opise Prosimo, počakajte ... - Uspešno ste se prijavili! - Prijava ni uspela! + Uspešna prijava! + Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. Dosežena je največja omejitev ponovnih poskusov! Prekličite nalaganje in poskusite znova. Ali želite izklopiti optimizacijo baterije? Nalaganje več kot 3 slik deluje bolj zanesljivo, ko je optimizacija baterije izklopljena. Za nemoteno nalaganje v nastavitvah izklopite optimizacijo baterije za aplikacijo Commons. \n\nMožni koraki za izklop optimizacije baterije:\n\n1. korak: Dotaknite se spodnjega gumba »Nastavitve«.\n\n 2. korak: Preklopite z »Ni optimizirano« na »Vse aplikacije«.\n\n3. korak: Poiščite »Commons« ali »fr.free.nrw.commons«.\n\n 4. korak: tapnite ga in izberite »Ne optimiziraj«.\n\n\n5. korak: Pritisnite »Končano«. - Avtentikacija ni uspela; prosimo, prijavite se znova + Avtentikacija ni uspela. Prosimo, prijavite se znova. Nalaganje se je začelo! Nalaganje na čakanju (vklopljen je način omejene povezanosti) Datoteka »%1$s« je naložena! @@ -117,17 +117,20 @@ Posnemi fotografijo Bližnje Moje naložitve + Kopiraj povezavo + Povezava je kopirana v odložišče Deli Ogled opisne strani Napis (obvezen) Prosimo, dodajte sliki napis Opis Napis - Prijava ni uspela – omrežna napaka + Prijava ni mogoča – omrežna napaka Preveč neuspešnih nalaganj. Prosimo, poskusite znova čez nekaj minut. Ta uporabnik je v Zbirki žal blokiran Navesti morate svojo dvofaktorsko avtentikacijsko kodo. - Prijava ni uspela + Na vaš e-poštni naslov je bila poslana potrditvena koda za prijavo. Prosimo, vnesite kodo za prijavo. + Prijava ni uspela Naloži Poimenuj skupino Spremembe @@ -135,6 +138,7 @@ Poišči kategorije Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) Shrani + Prelivni meni Osveži Seznam (Ni še naložitev) @@ -231,6 +235,7 @@ Postanite beta preizkuševalec Prijavite se na naš beta kanal v trgovini Google Play in pridobite zgodnji dostop do novih možnosti in popravkov hroščev Koda 2FA + Koda za preverjanje e-pošte Ali se res želite odjaviti? Nalaganje datoteke ni uspelo Najdena ni bila nobena podkategorija @@ -251,6 +256,7 @@ O orodju Nastavitve Odziv + Povratne informacije prek GitHuba Odjava Vadnica Obvestila @@ -284,14 +290,16 @@ Za podrobnosti glejte spletno stran Preskoči Prijava - Ali res želite preskočiti prijavo? - V prihodnosti se boste morali za nalaganje slik prijaviti. + Ali res želite preskočiti prijavo? + V prihodnosti se boste morali za nalaganje slik prijaviti. Za uporabo te možnosti se prijavite Kopiraj vikibesedilo v odložišče Vikibesedilo je skopirano v odložišče Bližnje mogoče ne bo pravilno delovalo. Kraj ni na voljo. + Internet ni na voljo. Prikazani so samo predpomnjeni kraji. Dostop do lokacije je bil zavrnjen. Za uporabo te možnosti ročno nastavite svojo lokacijo. Za prikaz seznama bložnjih krajev je potrebno dovoljenje + Za prikaz seznama bložnjih slik je potrebno dovoljenje Smeri Wikipodatki Wikipedija @@ -351,7 +359,7 @@ V Wikimedijini zbirki so zelo dobrodošle slike, ki prikazujejo tehniko ali kulturo. Pravilno ste odgovorili na %1$s vprašanj. Čestitamo! Za odgovor na vprašanje izberite eno od dveh možnosti. - Seja prijave je potekla. Poskusite se prijaviti znova. + Prijava je potekla. Prijavite se znova. Delite kviz s prijatelji! Nadaljuj Pravilen odgovor @@ -359,7 +367,8 @@ Ali je ta zaslonski posnetek primeren za naložitev? Delite aplikacijo Zasukaj - Napaka pri pridobivanju bližnjih krajev. + Ni bilo mogoče naložiti bližnjih krajev. + Na tem območju ni slik Tu okoli ni bližnjih krajev Napaka pri pridobivanju bližnjih spomenikov. Ni nedavnih iskanj @@ -371,11 +380,13 @@ Izbriši Dosežki Profil + Značke Statistika Prejete zahvale Izbrane slike Slike iz »Bližnji kraji« - Raven + Raven %d + %s (raven %s) Naložene slike Nevrnjene slike Uporabljene slike @@ -407,6 +418,7 @@ V vaši napravi ni mogoče najti nobene združljive aplikacije z zemljevidom. Za uporabo te možnosti namestite ustrezno aplikacijo z zemljevidom. Slike Kraji + Kategorije Dodaj/odstrani zaznamek Zaznamki Dodali niste nobenega zaznamka @@ -434,7 +446,7 @@ Nikoli več ne vprašaj Vprašaj za dovoljenje za lokacijo Vprašaj za dovoljenje za uporabo lokacije, ko je to potrebno za ogled obvestila o bližnjih krajih. - Nekaj je šlo narobe. Vaših dosežkov nismo mogli pridobiti. + Nekaj je šlo narobe. Dosežkov ni bilo mogoče pridobiti. Opravili ste toliko prispevkov, da jih naš sistem izračunavanja ne more razčleniti. To je vrhunski dosežek. Konec: Prikaži akcije @@ -450,7 +462,7 @@ Novi izbirnik fotografij Android lahko izgubi podatke o lokaciji. Omogočite ga, če ga želite uporabljati. Izklop te možnosti lahko aktivira novi izbirnik fotografij za Android. S tem tvegate izgubo podatkov o informacijah.\n\nZa več informacij kliknite »Preberi več«. Kampanj ne boste več videli. Če želite, lahko tovrstno obveščanje znova vklopite v nastavitvah. - Ta možnost zahteva omrežno povezavo. Prosimo, preverite vaše nastavitve povezave. + Ta funkcija zahteva omrežno povezavo. Prosimo, preverite vaše povezavne nastavitve. Pri obdelavi slike je prišlo do napake. Prosimo, poskusite znova! Pridobivam žeton za urejanje Dodajam predlogo za preverjanje kategorije @@ -488,6 +500,7 @@ Nimate neprebranih obvestil Ninate prebranih obvestil Delite dnevniške zapise z + Preverite svoj e-poštni nabiralnik Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake @@ -566,7 +579,7 @@ Ni bilo mogoče dodati koordinat. Ni bilo mogoče dodati opisov. Napisa ni bilo mogoče dodati. - Koordinat ni bilo mogoče pridobiti. + Koordinat slike ni bilo mogoče posodobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise Deli slike prek ... @@ -581,12 +594,13 @@ Potrebuje fotografijo Vrsta kraja: Most, muzej, hotel idr. - Pri prijavi je šlo nekaj narobe; ponastaviti morate geslo!! + Pri prijavi je šlo nekaj narobe. Ponastaviti morate geslo!! MEDIJ PODREJENI RAZREDI NADREJENI RAZREDI Najden bližnji kraj - Ali je to fotografija kraja %1$s? + So to slike %1$s? + Ali je to slika %1$s? Zaznamki Nastavitve Odstranjeno iz zaznamkov @@ -600,8 +614,10 @@ Nastavitev kraja ni bilo mogoče odpreti. Kraj vklopite ročno. Za najboljše zadetke izberite način Visoka natančnost. Vklopim kraj? + Vljudno vklopite lokacijske storitve, da bo aplikacija prikazovala vašo trenutno lokacijo. Za Bližnje je trebs vklopiti kraj - Za samodejno nastavitev lokacije morate omogočiti dostop do vaše trenutne lokacije. + Raziskovanje zemljevida potrebuje dovoljenje za lokacijo za prikaz bližnjih slik + Za samodejno nastavitev lokacije morate dodeliti dovoljenje za lokacijo. Ali ste sliki posneli na istem kraju? Ali želite uporabiti geografsko širino/dolžino desne slike? Naloži več Najden ni noben kraj. Poskusite spremeniti iskalne parametre. @@ -704,7 +720,7 @@ Bližnji zemljevidi morajo za pravilno delovanje prebrati STANJE TELEFONA Prispevki uporabnika: %s Dosežki uporabnika: %s - Ogled uporabniške strani + Ogled uporabniškega profila Uredi slike Uredi kategorije Napredne možnosti @@ -717,8 +733,8 @@ Dodaj lokacijo Iz te e-pošte odstranite vse podatke, ki jih ne želite javno deliti. Upoštevajte, da bodo vaš e-poštni naslov ter povezano ime in profilna slika javno vidni. Podrobnosti - Dosežki so na voljo samo v stabilni različici (preverite dokumentacijo razvijalcev). - Lestvica uvrstitev je na voljo samo v stabilni različici (glejte dokumentacijo razvijalcev). + Dosežki so na voljo samo v produkcijski različici. Preverite dokumentacijo razvijalcev. + Lestvica uvrstitev je na voljo samo v produkcijski različici. Glejte dokumentacijo razvijalcev. Prosimo, nalagajte samo slike, ki ste jih ustvarili sami. Naložnike avtorsko varovanega gradiva bomo blokirali. To velja tudi za betarazličico. Hvala za preizkušanje aplikacije! Prosimo, odizberite vse podatke, ki jih ne želite deliti javno. Raven API @@ -762,13 +778,72 @@ Oglejte si svoje dosežke Uredi sliko Uredi lokacijo + Lokacija posodobljena! + Odstrani lokacijo + Opozorilo o odstranitvi lokacije + Lokacija naredi slike bolj uporabne in jih je lažje najti. Ali res želite odstraniti lokacijo te slike? + Lokacija odstranjena! Zahvala avtorju Napaka pri pošiljanju zahvale avtorju. + Vaša prijava je potekla. Prosimo, prijavite se znova. + Na voljo ni nobena aplikacija za odpiranje datotek GPX + Datoteka je uspešno shranjena + Ali želite odpreti datoteko GPX? + Ali želite odpreti datoteko KML? + Shranjevanje datoteke KML ni uspelo. + Shranjevanje datoteke GPX ni uspelo. + Shranjevanje datoteke KML + Shranjevanje datoteke GPX %d izbrana slika %d izbrani sliki %d izbranih slik %d izbranih slik + Upoštevajte, da imajo vse slike v večkratnem nalaganju enake kategorije in upodobitve. Če slike nimajo skupnih upodobitev in kategorij, izvedite več ločenih nalaganj. + Opomba o večkratnih nalaganjih + Sporočite problem s tem predmetom v Wikipodatke + Prosimo, vnesite nekaj komentarjev + Pogovor + Napišite nekaj o predmetu »%1$s«. To bo javno vidno. + »%1$s« ne obstaja več. Slike tega ni več mogoče posneti. »%1$s« je na drugem kraju. Spodaj navedite pravi kraj in, če je mogoče, napišite pravilno zemljepisno širino in dolžino. + Drug problem ali informacija (razložite spodaj). + Vaše povratne informacije bodo objavljene na naslednji strani vikija: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilna aplikacija/Povratne informacije</a> + Ste prepričani, da želite preklicati vse nalaganja? + Preklic vseh nalaganj ... + Naložitve + Čakajoče + Neuspešno + Podatkov o kraju ni bilo mogoče naložiti + Izbriši mapo + Potrdi izbris + Ali ste prepričani, da želite izbrisati mapo %1$s, ki vsebuje %2$d predmetov? + Izbriši + Prekliči + Mapa %1$s je uspešno izbrisana + Izbris mape %1$s ni uspel + Napaka pri brisanju vsebine mape: %1$s + Pridobitev poti mape za bucket ID ni uspela: %1$d + Ta kraj še nima slike; posnamite jo! + Ta kraj že ima sliko. + Preverjam, ali ta kraj že ima sliko. + Napaka pri nalaganju + Ni najdenih uporab + Zbirka + Drugi vikiji + + Uporabe datotek + SingleWebViewActivity + Račun + Izgini račun + Opozorilo o izginjenju računa + Izginotje je <b>skrajni ukrep</b> in ga <b>uporabite samo, če želite za vedno prenehati z urejanjem</b>, in tudi, da skrijete čim več svojih preteklih povezav.\n\nIzbris računa v Wikimedijini zbirki se opravi tako, da spremenite ime svojega računa, da drugi ne morejo prepoznati vaših prispevkov. <b>Izbris ne zagotavlja popolne anonimnosti ali odstranitve prispevkov k projektom</b>. + Napis + Napis je kopiran v odložišče + Čestitamo, vse slike v tem albumu so naložene ali označene kot neprimerne za nalaganje. + Prikaži v Raziskuj + Prikaži v Bližnje + Ustvaril_a in naložil_a: %1$s + Ustvaril_a %1$s in naložil_a %2$s diff --git a/app/src/main/res/values-sr/error.xml b/app/src/main/res/values-sr/error.xml index a68c4efb2..4f5f91089 100644 --- a/app/src/main/res/values-sr/error.xml +++ b/app/src/main/res/values-sr/error.xml @@ -1,5 +1,6 @@ @@ -79,7 +80,7 @@ ⵉⴼⵔⵙ ⵉⵙⵉⴹⵏⵏ ⵜⵉⵙⵖⴰⵍ - ⴰⵏⵙⵙⵎⵔⵙ + ⴰⵏⵙⵎⵔⴰⵙ ⵙⵙⵉⴹⵏ ⴰⵙⴳⵯⵙⴰⵏ ⴰⵏⵎⵍⴰⵙⵙ diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ca11a4ec9..0434441c8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -136,6 +136,7 @@ 失敗次數過多。請於幾分鐘後重試。 很抱歉,該使用者已被維基共享資源封禁 必須提供您的雙重驗證代碼。 + 登入驗證碼已發送到您的電子郵件地址。請提供驗證碼以登入。 登入失敗 上傳 給這個集合命名 @@ -241,6 +242,7 @@ 成為 Beta 測試員 選擇加入我們在 Google Play 上的測試版頻道並儘早訪問新功能和錯誤修復 2FA 代碼 + 電子郵件驗證碼 您確定要登出嗎? 媒體圖片失敗 找不到子分類 @@ -842,4 +844,6 @@ 恭喜,該相簿中的所有圖片已上傳或標記為不可上傳。 在探索中顯示 顯示在附近 + 由%1$s建立與上傳 + 由%1$s建立,%2$s上傳 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 7543d98b7..5c7db0aae 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -25,6 +25,7 @@ * Liuxinyu970226 * Looong * McDutchie +* Mishidexfc * NACHOgao3 * Qiyue2001 * Shizhao @@ -168,6 +169,7 @@ 失败次数过多。请在几分钟后重试。 对不起,该用户已经被共享资源封禁 您必须提供您的双因素验证代码。 + 登录验证码已发送至您的邮箱,请提供该验证码进行登录。 登录失败 上传 命名这组图像 @@ -273,6 +275,7 @@ 成为测试版的测试者 加入我们在Google Play上的测试计划,更早获取新功能以及错误修复 2FA代码 + 电子邮件验证码 您真的想要退出么? 媒体图片失败 找不到子分类 @@ -873,4 +876,6 @@ 恭喜,专辑中的所有图片都已上传或标记为不上传。 在探索中显示 显示在附近 + 创建并上传者: %1$s + 由%1$s创建并由%2$s上传 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9b16512f..68dba88be 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,6 +111,7 @@ Too many unsuccessful attempts. Please try again in a few minutes. Sorry, this user has been blocked on Commons You must provide your two factor authentication code. + A login verification code has been sent to your email address. Please provide the code to log in. Log-in failed Upload Name this set @@ -218,6 +219,7 @@ Opt-in to our beta channel on Google Play and get early access to new features and bug fixes https://play.google.com/apps/testing/fr.free.nrw.commons 2FA Code + Email verification code Do you really want to logout? Media Image Failed No subcategories found @@ -830,6 +832,7 @@ Upload your first media by tapping on the add button. Talk Write something about the \'%1$s\' item. It will be publicly visible. \'%1$s\' does not exist anymore, no picture can ever be taken of it. + \'%1$s\' is at a different place. \'%1$s\' is at a different place. Please specify the correct place below, and if possible, write the correct latitude and longitude. Other problem or information (please explain below). Your feedback gets posted to the following wiki page: Commons:Mobile app/Feedback ]]> @@ -871,4 +874,6 @@ Upload your first media by tapping on the add button. Show in Explore Show in Nearby + Created and uploaded by: %1$s + Created by %1$s and uploaded by %2$s diff --git a/app/src/main/resources/queries/query_for_item.rq b/app/src/main/resources/queries/query_for_item.rq index 4a946ac96..2957b9b5b 100644 --- a/app/src/main/resources/queries/query_for_item.rq +++ b/app/src/main/resources/queries/query_for_item.rq @@ -10,6 +10,8 @@ SELECT (SAMPLE(?wikipediaArticle) AS ?wikipediaArticle) (SAMPLE(?commonsArticle) AS ?commonsArticle) (SAMPLE(?commonsCategory) AS ?commonsCategory) + (SAMPLE(?dateOfOfficialClosure) AS ?dateOfOfficialClosure) + (SAMPLE(?pointInTime) AS ?pointInTime) WHERE { SERVICE { values ?item { @@ -45,6 +47,8 @@ WHERE { # Get existence OPTIONAL {?item wdt:P576 ?destroyed} OPTIONAL {?item wdt:P582 ?endTime} + OPTIONAL {?item wdt:P3999 ?dateOfOfficialClosure} + OPTIONAL {?item wdt:P585 ?pointInTime} # Get Commons category OPTIONAL {?item wdt:P373 ?commonsCategory} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/ModelFunctions.kt b/app/src/test/kotlin/fr/free/nrw/commons/ModelFunctions.kt index 76f9c21d0..a3a201d94 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/ModelFunctions.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/ModelFunctions.kt @@ -50,6 +50,7 @@ fun media( licenseUrl: String? = "licenseUrl", author: String? = "creator", user: String? = "user", + creatorName: String? = null, pageId: String = "pageId", categories: List? = listOf("categories"), coordinates: LatLng? = LatLng(0.0, 0.0, 0.0f), @@ -67,6 +68,7 @@ fun media( licenseUrl, author, user, + creatorName, categories, coordinates, captions, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionViewHolderUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionViewHolderUnitTests.kt index 0171f2693..343cc4377 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionViewHolderUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/contributions/ContributionViewHolderUnitTests.kt @@ -8,6 +8,7 @@ import androidx.test.core.app.ApplicationProvider import com.facebook.drawee.backends.pipeline.Fresco import com.facebook.soloader.SoLoader import fr.free.nrw.commons.Media +import fr.free.nrw.commons.MediaDataExtractor import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.TestUtility.setFinalStatic @@ -46,6 +47,9 @@ class ContributionViewHolderUnitTests { @Mock private lateinit var mediaClient: MediaClient + @Mock + private lateinit var mediaDataExtractor: MediaDataExtractor + @Mock private lateinit var uri: Uri @@ -66,8 +70,9 @@ class ContributionViewHolderUnitTests { SoLoader.setInTestMode() Fresco.initialize(ApplicationProvider.getApplicationContext()) activity = Robolectric.buildActivity(ProfileActivity::class.java).create().get() + compositeDisposable = CompositeDisposable() parent = LayoutInflater.from(activity).inflate(R.layout.layout_contribution, null) - contributionViewHolder = ContributionViewHolder(parent, callback, mediaClient) + contributionViewHolder = ContributionViewHolder(parent, callback, compositeDisposable, mediaClient, mediaDataExtractor) bindind = LayoutContributionBinding.bind(parent) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt index 8a3022a35..c52b38ba0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/media/MediaConverterTest.kt @@ -10,6 +10,7 @@ import org.junit.Before import org.junit.Test import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import java.lang.IllegalArgumentException @@ -42,23 +43,61 @@ class MediaConverterTest { @Test fun testConvertIfThumbUrlBlank() { - Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata) - Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("") - Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") - Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl") - Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(imageInfo.getMetadata()).thenReturn(metadata) + `when`(imageInfo.getThumbUrl()).thenReturn("") + `when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + `when`(metadata.licenseUrl()).thenReturn("licenseUrl") + `when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(metadata.artist()).thenReturn("Foo Bar") media = mediaConverter.convert(page, entity, imageInfo) assertEquals(media.thumbUrl, media.imageUrl, "originalUrl") } @Test fun testConvertIfThumbUrlNotBlank() { - Mockito.`when`(imageInfo.getMetadata()).thenReturn(metadata) - Mockito.`when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl") - Mockito.`when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") - Mockito.`when`(imageInfo.getMetadata()?.licenseUrl()).thenReturn("licenseUrl") - Mockito.`when`(imageInfo.getMetadata()?.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(imageInfo.getMetadata()).thenReturn(metadata) + `when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl") + `when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + `when`(metadata.licenseUrl()).thenReturn("licenseUrl") + `when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(metadata.artist()).thenReturn("Foo Bar") media = mediaConverter.convert(page, entity, imageInfo) assertEquals(media.thumbUrl, "thumbUrl") } + + @Test + fun `test converting artist value (author) with html links`() { + `when`(imageInfo.getMetadata()).thenReturn(metadata) + `when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl") + `when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + `when`(metadata.licenseUrl()).thenReturn("licenseUrl") + `when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(metadata.artist()).thenReturn("Foo Bar") + // Artist values like above is very common, found in file pages created via UploadWizard + media = mediaConverter.convert(page, entity, imageInfo) + assertEquals("Foo Bar", media.author) + } + + @Test + fun `test convert artist value (author) in plain text`() { + `when`(imageInfo.getMetadata()).thenReturn(metadata) + `when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl") + `when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + `when`(metadata.licenseUrl()).thenReturn("licenseUrl") + `when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(metadata.artist()).thenReturn("Foo Bar") + media = mediaConverter.convert(page, entity, imageInfo) + assertEquals("Foo Bar", media.author) + } + @Test + fun `test convert artist value (author) containing red link`() { + `when`(imageInfo.getMetadata()).thenReturn(metadata) + `when`(imageInfo.getThumbUrl()).thenReturn("thumbUrl") + `when`(imageInfo.getOriginalUrl()).thenReturn("originalUrl") + `when`(metadata.licenseUrl()).thenReturn("licenseUrl") + `when`(metadata.dateTime()).thenReturn("yyyy-MM-dd HH:mm:ss") + `when`(metadata.artist()).thenReturn("Foo") + media = mediaConverter.convert(page, entity, imageInfo) + assertEquals("Foo", media.author) + } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt index 1173d09b0..97fe68862 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadActivityUnitTests.kt @@ -262,15 +262,4 @@ class UploadActivityUnitTests { method.isAccessible = true method.invoke(activity) } - - @Test - @Throws(Exception::class) - fun testOnBackPressed() { - val method: Method = - UploadActivity::class.java.getDeclaredMethod( - "onBackPressed", - ) - method.isAccessible = true - method.invoke(activity) - } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/utils/MediaAttributionUtilTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/utils/MediaAttributionUtilTest.kt new file mode 100644 index 000000000..cd1e41321 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/utils/MediaAttributionUtilTest.kt @@ -0,0 +1,78 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.media.IdAndLabels +import org.junit.Assert.* +import org.junit.Before + +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [21], application = TestCommonsApplication::class, qualifiers="en-rUS") +class MediaAttributionUtilTest { + + @Mock + private lateinit var appContext: Context + + @Before + fun setup() { + appContext = ApplicationProvider.getApplicationContext() + } + + @Test + fun getTagLineWithUploaderOnly() { + val media = mock(Media::class.java) + whenever(media.user).thenReturn("TestUploader") + whenever(media.author).thenReturn(null) + assertEquals("Uploaded by: TestUploader", + MediaAttributionUtil.getTagLine(media, appContext)) + } + + @Test + fun `get tag line from same author and uploader`() { + val media = mock(Media::class.java) + whenever(media.user).thenReturn("TestUser") + whenever(media.getAttributedAuthor()).thenReturn("TestUser") + assertEquals("Created and uploaded by: TestUser", + MediaAttributionUtil.getTagLine(media, appContext)) + } + + @Test + fun `get creator name from EN label`() { + assertEquals("FooBar", + MediaAttributionUtil.getCreatorName(listOf(IdAndLabels("Q1", mapOf("en" to "FooBar"))))) + } + + @Test + fun `get creator name from ES label`() { + assertEquals("FooBar", + MediaAttributionUtil.getCreatorName(listOf(IdAndLabels("Q2", mapOf("es" to "FooBar"))))) + } + + @Test + fun `get creator name from EN label and ignore ES label`() { + assertEquals("Bar", + MediaAttributionUtil.getCreatorName(listOf( + IdAndLabels("Q3", mapOf("en" to "Bar", "es" to "Foo"))))) + } + + @Test + fun `get creator name from two creators`() { + val name = MediaAttributionUtil.getCreatorName(listOf( + IdAndLabels("Q1", mapOf("en" to "Foo")), + IdAndLabels("Q1", mapOf("en" to "Bar")) + )) + assertNotNull(name) + assertTrue(name!!.contains("Foo")) + assertTrue(name.contains("Bar")) + } +} \ No newline at end of file