From cdc4f89da551b9be51ee17f488a6c766e6decbd0 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sun, 27 Oct 2024 19:08:40 +0530 Subject: [PATCH 001/231] Database bug fix (#5902) * make database function calls suspending and update room version * replace MainScope with coroutineScope for database operations * add suspend keyword and refactor code --- .../database/NotForUploadStatusDao.kt | 10 +++++----- .../customselector/database/UploadedStatusDao.kt | 14 +++++++------- .../customselector/ui/selector/ImageLoader.kt | 4 ++-- .../fr/free/nrw/commons/nearby/PlaceDao.java | 12 +++--------- .../nrw/commons/upload/depicts/DepictsDao.kt | 16 ++++++++-------- .../nrw/commons/upload/worker/UploadWorker.kt | 4 ++-- gradle.properties | 2 +- 7 files changed, 28 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt index 872388f40d..b75a6e1d4f 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/NotForUploadStatusDao.kt @@ -15,19 +15,19 @@ abstract class NotForUploadStatusDao { * Insert into Not For Upload status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(notForUploadStatus: NotForUploadStatus) + abstract suspend fun insert(notForUploadStatus: NotForUploadStatus) /** * Delete Not For Upload status entry. */ @Delete - abstract fun delete(notForUploadStatus: NotForUploadStatus) + abstract suspend fun delete(notForUploadStatus: NotForUploadStatus) /** * Query Not For Upload status with image sha1. */ @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? + abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus? /** * Asynchronous image sha1 query. @@ -38,7 +38,7 @@ abstract class NotForUploadStatusDao { * Deletion Not For Upload status with image sha1. */ @Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun deleteWithImageSHA1(imageSHA1: String) + abstract suspend fun deleteWithImageSHA1(imageSHA1: String) /** * Asynchronous image sha1 deletion. @@ -49,5 +49,5 @@ abstract class NotForUploadStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun find(imageSHA1: String): Int + abstract suspend fun find(imageSHA1: String): Int } diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt index 03cbb176fe..378af5b8db 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/database/UploadedStatusDao.kt @@ -17,31 +17,31 @@ abstract class UploadedStatusDao { * Insert into uploaded status. */ @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(uploadedStatus: UploadedStatus) + abstract suspend fun insert(uploadedStatus: UploadedStatus) /** * Update uploaded status entry. */ @Update - abstract fun update(uploadedStatus: UploadedStatus) + abstract suspend fun update(uploadedStatus: UploadedStatus) /** * Delete uploaded status entry. */ @Delete - abstract fun delete(uploadedStatus: UploadedStatus) + abstract suspend fun delete(uploadedStatus: UploadedStatus) /** * Query uploaded status with image sha1. */ @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") - abstract fun getFromImageSHA1(imageSHA1: String): UploadedStatus? + abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus? /** * Query uploaded status with modified image sha1. */ @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") - abstract fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? + abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus? /** * Asynchronous insert into uploaded status table. @@ -55,7 +55,7 @@ abstract class UploadedStatusDao { * Check whether the imageSHA1 is present in database */ @Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ") - abstract fun findByImageSHA1( + abstract suspend fun findByImageSHA1( imageSHA1: String, imageResult: Boolean, ): Int @@ -66,7 +66,7 @@ abstract class UploadedStatusDao { @Query( "SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ", ) - abstract fun findByModifiedImageSHA1( + abstract suspend fun findByModifiedImageSHA1( modifiedImageSHA1: String, modifiedImageResult: Boolean, ): Int diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 1fb5c59537..95c768c1c2 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt @@ -17,7 +17,7 @@ import fr.free.nrw.commons.utils.CustomSelectorUtils import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileExistsOnCommonsUsingSHA1 import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.MainScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.Calendar import java.util.concurrent.TimeUnit @@ -65,7 +65,7 @@ class ImageLoader /** * Coroutine Scope. */ - private val scope: CoroutineScope = MainScope() + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) /** * Query image and setUp the view. diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java index 7babee3b71..9e42921142 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceDao.java @@ -37,15 +37,11 @@ public abstract class PlaceDao { */ public Completable save(final Place place) { return Completable - .fromAction(() -> { - saveSynchronous(place); - }); + .fromAction(() -> saveSynchronous(place)); } /** * Deletes all Place objects from the database. - * - * @return A Completable that completes once the deletion operation is done. */ @Query("DELETE FROM place") public abstract void deleteAllSynchronous(); @@ -53,11 +49,9 @@ public Completable save(final Place place) { /** * Deletes all Place objects from the database. * + * @return A Completable that completes once the deletion operation is done. */ public Completable deleteAll() { - return Completable - .fromAction(() -> { - deleteAllSynchronous(); - }); + return Completable.fromAction(this::deleteAllSynchronous); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt index c20d65abf6..139b67d591 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsDao.kt @@ -22,21 +22,21 @@ abstract class DepictsDao { private val maxItemsAllowed = 10 @Insert(onConflict = OnConflictStrategy.REPLACE) - abstract fun insert(depictedItem: Depicts) + abstract suspend fun insert(depictedItem: Depicts) @Query("Select * From depicts_table order by lastUsed DESC") - abstract fun getAllDepicts(): List + abstract suspend fun getAllDepicts(): List @Query("Select * From depicts_table order by lastUsed DESC LIMIT :n OFFSET 10") - abstract fun getDepictsForDeletion(n: Int): List + abstract suspend fun getDepictsForDeletion(n: Int): List @Delete - abstract fun delete(depicts: Depicts) + abstract suspend fun delete(depicts: Depicts) /** * Gets all Depicts objects from the database, ordered by lastUsed in descending order. * - * @return A list of Depicts objects. + * @return Deferred list of Depicts objects. */ fun depictsList(): Deferred> = CoroutineScope(Dispatchers.IO).async { @@ -48,7 +48,7 @@ abstract class DepictsDao { * * @param depictedItem The Depicts object to insert. */ - private fun insertDepict(depictedItem: Depicts) = + fun insertDepict(depictedItem: Depicts) = CoroutineScope(Dispatchers.IO).launch { insert(depictedItem) } @@ -59,7 +59,7 @@ abstract class DepictsDao { * @param n The number of depicts to delete. * @return A list of Depicts objects to delete. */ - private suspend fun depictsForDeletion(n: Int): Deferred> = + fun depictsForDeletion(n: Int): Deferred> = CoroutineScope(Dispatchers.IO).async { getDepictsForDeletion(n) } @@ -69,7 +69,7 @@ abstract class DepictsDao { * * @param depicts The Depicts object to delete. */ - private suspend fun deleteDepicts(depicts: Depicts) = + fun deleteDepicts(depicts: Depicts) = CoroutineScope(Dispatchers.IO).launch { delete(depicts) } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 15a0494892..144c503bba 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -41,8 +41,8 @@ import fr.free.nrw.commons.upload.UploadProgressActivity import fr.free.nrw.commons.upload.UploadResult import fr.free.nrw.commons.wikidata.WikidataEditService import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber @@ -534,7 +534,7 @@ class UploadWorker( contribution.contentUri?.let { val imageSha1 = contribution.imageSHA1.toString() val modifiedSha1 = fileUtilsWrapper.getSHA1(fileUtilsWrapper.getFileInputStream(contribution.localUri?.path)) - MainScope().launch { + CoroutineScope(Dispatchers.IO).launch { uploadedStatusDao.insertUploaded( UploadedStatus( imageSha1, diff --git a/gradle.properties b/gradle.properties index 9ca154b750..0aee97f4eb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,7 +20,7 @@ android.enableR8.fullMode=false KOTLIN_VERSION=1.9.22 LEAK_CANARY_VERSION=2.10 DAGGER_VERSION=2.23 -ROOM_VERSION=2.5.0 +ROOM_VERSION=2.6.1 PREFERENCE_VERSION=1.1.0 CORE_KTX_VERSION=1.9.0 ADAPTER_DELEGATES_VERSION=4.3.0 From 522f1fe1922651d5a9ae11d11ec80c9a2d5a621b Mon Sep 17 00:00:00 2001 From: u7119288 <141695960+baijun6@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:59:09 +1100 Subject: [PATCH 002/231] Partial fixes for errors and warnings reported by ./gradlew lint (#5885) * BaseMarker.kt: removed unneeded cast * TransformImageImpl.kt: removed unreachable code * ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView * ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView * DescriptionEditActivity.kt: removed unnecessary non-null assertion (!!) on a non-null receiver of type DescriptionEditHelper * Media.kt: Property would not be serialized into a 'Parcel'. Added '@IgnoredOnParcel' annotation to remove the warning * ZoomableActivity.kt: removed Unnecessary non-null assertion (!!) on a non-null receiver of type ZoomableDraweeView * CategoryClient.kt: removed condition 'page.categoryInfo() == null' as it's always 'false' * DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type SessionManager * DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type DescriptionEditHelper * WikidataFeedback.kt: removed unneeded cast * FailedUploadsFragment.kt: removed unneeded non-null assertion (!!) * PendingUploadsFragment.kt: removed unneeded non-null assertion (!!) * AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * FileUtils.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * LocationPickerActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * MediaDetailFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * NearbyFilterSearchRecyclerViewAdapter.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ProfileActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * RecentSearchesFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ReviewActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * SearchActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * UploadMediaPresenter.java: Changed toUpperCase to toLowerCase(Locale.ROOT) * CategoriesMediaFragment.kt: Changed arguments!! to requireArguments() * ChildDepictionsFragment.kt: Changed arguments!! to requireArguments() * DepictedImagesFragment.kt: Changed arguments!! to requireArguments() * DepictsFragment: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity())) to requireActivity() * ParentCategoriesFragment.kt: Changed arguments!! to requireArguments() * ParentCategoriesFragment.kt: Changed arguments!! to requireArguments() * SubCategoriesFragment.kt: Changed arguments!! to requireArguments() * SubCategoriesFragment.kt: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity()) to requireActivity() * UploadMediaDetailFragment.java: Changed arguments!! to requireArguments() * WikipediaInstructionsDialogFragment.kt: Changed arguments!! to requireArguments() * BookmarkItemsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex() * BookmarkLocationsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex() * AndroidManifest.xml: Removed redundant label android:label="@string/app_name" * bs\strings.xml: Added missing few quantity * hr\strings.xml: Added missing few quantity * hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63 * Revert "hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63" This reverts commit 47232466ab4fc3ac91958f888bc5033fefc699a5. * cy\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63 * sr\strings.xml: Added missing few quantities for lines 35 to 70 * ro\strings.xml: Added missing few quantity and removed not needed zero for * cs\strings.xml: Added missing few many missing quantities for lines 33 - 74 * lt\strings.xml: Added missing few, many missing quantities for lines 34 - 58 * lt\strings.xml: Replaced . . . with ... * ca\strings.xml: Added missing many quantities for lines 21 - 51, replaced . . . with ... * ser\strings.xml: Added missing few quantities, replaced . . . with ... * br\strings.xml: Added missing two, few, many quantities * pt\strings.xml: Added missing many quantity, changed . . . to ... and ignored typo as it is correct for European Portuguese * it\strings.xml: changed . . . to ... * pt\strings.xml: fixed many quantity * ca\strings.xml: fixed many quantity * sr\strings.xml: fixed many quantity * cy\strings.xml: corrected quantities for "share_license_summary * fr\strings.xml: changed . . . to ... and add many quantities using the other quantity * fr\strings.xml: changed . . . to ... and add many quantities using the other quantity. Fixed some typos, added ignore for correct spellings but has warning * getColumnIndex(): added @SuppressLint("Range") as -1 is expected result for column name doesn't exist * values-b+sr+Latn\strings.xml: changed . . . to ... * Revert "values-b+sr+Latn\strings.xml: changed . . . to ..." This reverts commit 95b909c29f7f96fe0a885a22daa7fd1a3568e058. * values-b+roa+tara\strings.xml: changed . . . to ... * Revert "values-b+roa+tara\strings.xml: changed . . . to ..." This reverts commit b5db1a3e68a9abfb95b4fc3f6219b507ead16d16. * values-b+roa+tara\strings.xml: changed . . . to ... * values-b+sr+Latn\strings.xml: changed . . . to ..., add few based on other quantity. Ignored one ImpliedQuantity warning as it is correct. * it\strings.xml: changed . . . to ..., add many based on other quantity. * pt-rBR\strings.xml: changed . . . to ..., add many based on other quantity. Fixed typos, ignored warning for "one" quantity as translation didn't use number * si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity * si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity. Fixed wrong %1$d. Changed . . . to ... * mk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use 1 * sl\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * ru\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * uk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * is\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * strings.xml: changed . . . to ... * af\strings.xml: removed not needed zero quantity * de\strings.xml: fixed duplicate word typo * diq\strings.xml: changed - to dash (-) * hi\strings.xml: removed not needed zero * in\strings.xml: removed not needed one quantity * iw\strings.xml: removed not needed many quantity * ja\strings.xml: removed not needed one quantity * ko\strings.xml: removed not needed one quantity * ky\strings.xml: removed not needed one quantity * mr\strings.xml: removed not needed zero quantity * my\strings.xml: removed not needed one quantity * su\strings.xml: removed not needed one quantity * th\strings.xml: removed not needed one and zero quantity * zh\strings.xml: removed not needed one quantity * activity_description_edit.xml: changed android:tint to app:tint, changed layout_alignParentRight to layout_alignParentEnd * bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed to margin layout * bottom_sheet_item_layout.xml: changed android:tint to app:tint, added focusable * bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * item_place.xml.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * layout_campagin.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * layout_contribution.xml.xml: changed android:tint to app:tint * nearby_card_view.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * nearby_row_button.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * toolbar_location_picker.xml: changed android:tint to app:tint --------- Co-authored-by: Nicolas Raoul --- app/src/main/AndroidManifest.xml | 3 +- .../java/fr/free/nrw/commons/BaseMarker.kt | 2 +- .../LocationPickerActivity.java | 4 +- .../main/java/fr/free/nrw/commons/Media.kt | 2 + .../bookmarks/items/BookmarkItemsDao.java | 2 + .../locations/BookmarkLocationsDao.java | 2 + .../pictures/BookmarkPicturesDao.java | 2 + .../nrw/commons/category/CategoryClient.kt | 2 +- .../nrw/commons/category/CategoryDao.java | 2 + .../WikipediaInstructionsDialogFragment.kt | 2 +- .../description/DescriptionEditActivity.kt | 6 +- .../nrw/commons/edit/TransformImageImpl.kt | 1 - .../nrw/commons/explore/ExploreFragment.java | 7 +- .../nrw/commons/explore/SearchActivity.java | 7 +- .../media/CategoriesMediaFragment.kt | 2 +- .../parent/ParentCategoriesFragment.kt | 2 +- .../categories/sub/SubCategoriesFragment.kt | 2 +- .../child/ChildDepictionsFragment.kt | 4 +- .../media/DepictedImagesFragment.kt | 2 +- .../parent/ParentDepictionsFragment.kt | 4 +- .../recentsearches/RecentSearchesDao.java | 2 + .../RecentSearchesFragment.java | 3 +- .../commons/media/MediaDetailFragment.java | 4 +- .../nrw/commons/media/ZoomableActivity.kt | 18 ++--- ...NearbyFilterSearchRecyclerViewAdapter.java | 5 +- .../nrw/commons/nearby/WikidataFeedback.kt | 2 +- .../nrw/commons/profile/ProfileActivity.java | 5 +- .../achievements/AchievementsFragment.java | 3 +- .../recentlanguages/RecentLanguagesDao.java | 2 + .../nrw/commons/review/ReviewActivity.java | 3 +- .../commons/upload/FailedUploadsFragment.kt | 6 +- .../fr/free/nrw/commons/upload/FileUtils.java | 3 +- .../commons/upload/PendingUploadsFragment.kt | 4 +- .../categories/UploadCategoriesFragment.java | 6 +- .../upload/depicts/DepictsFragment.java | 6 +- .../UploadMediaDetailFragment.java | 2 +- .../mediaDetails/UploadMediaPresenter.java | 4 +- .../res/layout/activity_description_edit.xml | 6 +- .../layout/bottom_sheet_details_explore.xml | 12 ++-- .../res/layout/bottom_sheet_item_layout.xml | 4 +- .../main/res/layout/fragment_achievements.xml | 67 +++++-------------- app/src/main/res/layout/item_place.xml | 13 +--- app/src/main/res/layout/layout_campagin.xml | 19 ++---- .../main/res/layout/layout_contribution.xml | 4 +- app/src/main/res/layout/nearby_card_view.xml | 16 ++--- app/src/main/res/layout/nearby_row_button.xml | 20 +++--- .../res/layout/toolbar_location_picker.xml | 4 +- app/src/main/res/values-ab/strings.xml | 4 +- app/src/main/res/values-af/strings.xml | 3 +- app/src/main/res/values-anp/strings.xml | 10 +-- app/src/main/res/values-ar/strings.xml | 8 +-- app/src/main/res/values-as/strings.xml | 4 +- app/src/main/res/values-ast/strings.xml | 4 +- app/src/main/res/values-az/strings.xml | 2 +- .../main/res/values-b+roa+tara/strings.xml | 6 +- app/src/main/res/values-b+sr+Latn/strings.xml | 19 ++++-- app/src/main/res/values-ba/strings.xml | 8 +-- app/src/main/res/values-ban/strings.xml | 6 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-blk/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 6 ++ app/src/main/res/values-bs/strings.xml | 5 +- app/src/main/res/values-ca/strings.xml | 8 ++- app/src/main/res/values-ce/strings.xml | 8 +-- app/src/main/res/values-cs/strings.xml | 15 ++++- app/src/main/res/values-csb/strings.xml | 6 +- app/src/main/res/values-cy/strings.xml | 19 ++++++ app/src/main/res/values-da/strings.xml | 10 +-- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-diq/strings.xml | 8 +-- app/src/main/res/values-el/strings.xml | 8 +-- app/src/main/res/values-eo/strings.xml | 16 ++--- app/src/main/res/values-es/strings.xml | 38 +++++++---- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 10 +-- app/src/main/res/values-fi/strings.xml | 10 +-- app/src/main/res/values-fr/strings.xml | 36 ++++++---- app/src/main/res/values-gcr/strings.xml | 6 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 7 +- app/src/main/res/values-hr/strings.xml | 17 +++-- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 18 +++-- app/src/main/res/values-io/strings.xml | 14 ++-- app/src/main/res/values-is/strings.xml | 8 +-- app/src/main/res/values-it/strings.xml | 15 ++++- app/src/main/res/values-iw/strings.xml | 26 +++---- app/src/main/res/values-ja/strings.xml | 5 +- app/src/main/res/values-kab/strings.xml | 8 +-- app/src/main/res/values-ko/strings.xml | 14 ++-- app/src/main/res/values-krc/strings.xml | 12 ++-- app/src/main/res/values-ku/strings.xml | 6 +- app/src/main/res/values-kum/strings.xml | 2 +- app/src/main/res/values-kus/strings.xml | 18 ++--- app/src/main/res/values-ky/strings.xml | 3 +- app/src/main/res/values-lb/strings.xml | 10 +-- app/src/main/res/values-li/strings.xml | 8 +-- app/src/main/res/values-lt/strings.xml | 21 ++++-- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 12 ++-- app/src/main/res/values-mni/strings.xml | 4 +- app/src/main/res/values-mnw/strings.xml | 4 +- app/src/main/res/values-mr/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 14 ++-- app/src/main/res/values-nl/strings.xml | 6 +- app/src/main/res/values-nqo/strings.xml | 20 +++--- app/src/main/res/values-oc/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 13 ++-- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 6 +- app/src/main/res/values-ps/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++--- app/src/main/res/values-pt/strings.xml | 32 ++++++--- app/src/main/res/values-ro/strings.xml | 10 +-- app/src/main/res/values-ru/strings.xml | 14 ++-- app/src/main/res/values-sd/strings.xml | 4 +- app/src/main/res/values-se/strings.xml | 8 +-- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-si/strings.xml | 11 ++- app/src/main/res/values-sk/strings.xml | 14 ++-- app/src/main/res/values-sl/strings.xml | 30 ++++----- app/src/main/res/values-sr/strings.xml | 17 +++-- app/src/main/res/values-su/strings.xml | 13 +--- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tcy/strings.xml | 4 +- app/src/main/res/values-te/strings.xml | 12 ++-- app/src/main/res/values-th/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-uk/strings.xml | 6 +- app/src/main/res/values-uz/strings.xml | 8 +-- app/src/main/res/values-vec/strings.xml | 10 +-- app/src/main/res/values-xal/strings.xml | 8 +-- app/src/main/res/values-xmf/strings.xml | 6 +- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 17 ++--- 137 files changed, 617 insertions(+), 572 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89ed630d84..29f280c9ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -99,7 +99,6 @@ android:exported="true" android:hardwareAccelerated="false" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" android:windowSoftInputMode="adjustResize"> @@ -122,7 +121,7 @@ android:name=".contributions.MainActivity" android:configChanges="screenSize|keyboard|orientation" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" /> + /> diff --git a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt index 1daadb5a16..28b01d6031 100644 --- a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt +++ b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt @@ -46,7 +46,7 @@ class BaseMarker { val drawable: Drawable = context.resources.getDrawable(drawableResId) icon = if (drawable is BitmapDrawable) { - (drawable as BitmapDrawable).bitmap + drawable.bitmap } else { val bitmap = Bitmap.createBitmap( diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java index 8c54fd292b..2f05705bac 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java @@ -53,6 +53,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; @@ -301,7 +302,8 @@ private void bindViews() { modifyLocationButton = findViewById(R.id.modify_location); removeLocationButton = findViewById(R.id.remove_location); showInMapButton = findViewById(R.id.show_in_map); - showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); + showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase( + Locale.ROOT)); shadow = findViewById(R.id.location_picker_image_view_shadow); } 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 93efac7b23..025302cfdb 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.kt +++ b/app/src/main/java/fr/free/nrw/commons/Media.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons import android.os.Parcelable import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.wikidata.model.page.PageTitle +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.util.Date import java.util.Locale @@ -124,6 +125,7 @@ class Media constructor( * Gets the categories the file falls under. * @return file categories as an ArrayList of Strings */ + @IgnoredOnParcel var addedCategories: List? = null // TODO added categories should be removed. It is added for a short fix. On category update, // categories should be re-fetched instead diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java index 70c3708368..6788a8290b 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.items; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -134,6 +135,7 @@ public boolean findBookmarkItem(final String depictedItemID) { * @param cursor : Object for storing database data * @return DepictedItem */ + @SuppressLint("Range") DepictedItem fromCursor(final Cursor cursor) { final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); final String description diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java index 850b953e94..fe4f603f49 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.locations; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -146,6 +147,7 @@ public boolean findBookmarkLocation(Place bookmarkLocation) { return false; } + @SuppressLint("Range") @NonNull Place fromCursor(final Cursor cursor) { final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java index a56a39ba20..c214ae996e 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.pictures; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -150,6 +151,7 @@ public boolean findBookmark(Bookmark bookmark) { return false; } + @SuppressLint("Range") @NonNull Bookmark fromCursor(Cursor cursor) { String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME)); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 64463d8263..992c4ed1cf 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -124,7 +124,7 @@ class CategoryClient }.map { it .filter { page -> - page.categoryInfo() == null || !page.categoryInfo().isHidden + !page.categoryInfo().isHidden }.map { CategoryItem( it.title().replace(CATEGORY_PREFIX, ""), diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index b638fc5081..3cd60ac81a 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.category; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -111,6 +112,7 @@ List recentCategories(int limit) { } @NonNull + @SuppressLint("Range") Category fromCursor(Cursor cursor) { // Hardcoding column positions! return new Category( diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt index 77e52e1dbe..86cda2cf37 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt @@ -22,7 +22,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() { ) = DialogAddToWikipediaInstructionsBinding .inflate(inflater, container, false) .apply { - val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION) + val contribution: Contribution? = requireArguments().getParcelable(ARG_CONTRIBUTION) tvWikicode.setText(contribution?.media?.wikiCode) instructionsCancel.setOnClickListener { dismiss() } instructionsConfirm.setOnClickListener { diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 7ed598637b..fa4349dbf4 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -237,7 +237,7 @@ class DescriptionEditActivity : ) { try { descriptionEditHelper - ?.addDescription( + .addDescription( applicationContext, media, updatedWikiText, @@ -250,7 +250,7 @@ class DescriptionEditActivity : ) } } catch (e: InvalidLoginTokenException) { - val username: String? = sessionManager?.userName + val username: String? = sessionManager.userName val logoutListener = CommonsApplication.BaseLogoutListener( this, @@ -268,7 +268,7 @@ class DescriptionEditActivity : for (mediaDetail in uploadMediaDetails) { try { compositeDisposable.add( - descriptionEditHelper!! + descriptionEditHelper .addCaption( applicationContext, media, diff --git a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt index b596196915..c3db1a5a0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt +++ b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt @@ -65,7 +65,6 @@ class TransformImageImpl : TransformImage { } catch (e: LLJTranException) { Timber.tag("Error").d(e) return null - false } if (rotated) { 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 c66cd51631..26c8dd82b2 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 @@ -22,6 +22,7 @@ import fr.free.nrw.commons.utils.ActivityUtils; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; @@ -112,13 +113,13 @@ public void setTabs() { mobileRootFragment = new ExploreListRootFragment(mobileArguments); mapRootFragment = new ExploreMapRootFragment(mapArguments); fragmentList.add(featuredRootFragment); - titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase(Locale.ROOT)); fragmentList.add(mobileRootFragment); - titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase(Locale.ROOT)); fragmentList.add(mapRootFragment); - titleList.add(getString(R.string.explore_tab_title_map).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT)); ((MainActivity)getActivity()).showTabs(); ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 7717f2deb7..abb27184f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import timber.log.Timber; @@ -95,11 +96,11 @@ public void setTabs() { searchDepictionsFragment = new SearchDepictionsFragment(); searchCategoryFragment= new SearchCategoryFragment(); fragmentList.add(searchMediaFragment); - titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase(Locale.ROOT)); fragmentList.add(searchCategoryFragment); - titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase(Locale.ROOT)); fragmentList.add(searchDepictionsFragment); - titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase(Locale.ROOT)); viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt index 6de1248b4a..765abd698e 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt @@ -18,6 +18,6 @@ class CategoriesMediaFragment : PageableMediaFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt index 6ceccf6075..c43e1c6bd9 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt @@ -21,6 +21,6 @@ class ParentCategoriesFragment : PageableCategoryFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt index 19fe52beb0..8fbc830391 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt @@ -20,6 +20,6 @@ class SubCategoriesFragment : PageableCategoryFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt index 5275362997..4f13b1be8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt @@ -13,13 +13,13 @@ class ChildDepictionsFragment : PageableDepictionsFragment() { override val injectedPresenter get() = presenter - override fun getEmptyText(query: String) = getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!) + override fun getEmptyText(query: String) = getString(R.string.no_child_classes, requireArguments().getString("wikidataItemName")!!) override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt index cc1b664b2a..4cdb0e4618 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt @@ -17,6 +17,6 @@ class DepictedImagesFragment : PageableMediaFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt index 52a5aff5d8..cf739a07db 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt @@ -13,13 +13,13 @@ class ParentDepictionsFragment : PageableDepictionsFragment() { override val injectedPresenter get() = presenter - override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, arguments!!.getString("wikidataItemName")!!) + override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, requireArguments().getString("wikidataItemName")!!) override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java index 9f12639dd2..cee8a25ae8 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.explore.recentsearches; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -178,6 +179,7 @@ public List recentSearches(int limit) { * @return RecentSearch object */ @NonNull + @SuppressLint("Range") RecentSearch fromCursor(Cursor cursor) { // Hardcoding column positions! return new RecentSearch( diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java index cd98651f01..0db1e55395 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java @@ -15,6 +15,7 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.SearchActivity; import java.util.List; +import java.util.Locale; import javax.inject.Inject; @@ -90,7 +91,7 @@ private void setDeleteRecentPositiveButton(@NonNull final Context context, private void showDeleteAlertDialog(@NonNull final Context context, final int position) { new AlertDialog.Builder(context) .setMessage(R.string.delete_search_dialog) - .setPositiveButton(getString(R.string.delete).toUpperCase(), + .setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT), ((dialog, which) -> setDeletePositiveButton(context, dialog, position))) .setNegativeButton(android.R.string.cancel, null) .create() diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index dd0829a1b5..142d8379c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -600,8 +600,8 @@ private void updateToDoWarning() { // Check if the presented category is about need of category if (categoriesPresent) { for (String category : media.getCategories()) { - if (category.toLowerCase().contains(CATEGORY_NEEDING_CATEGORIES) || - category.toLowerCase().contains(CATEGORY_UNCATEGORISED)) { + if (category.toLowerCase(Locale.ROOT).contains(CATEGORY_NEEDING_CATEGORIES) || + category.toLowerCase(Locale.ROOT).contains(CATEGORY_UNCATEGORISED)) { categoriesPresent = false; } break; diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt index d08e3048c3..14b5788c24 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt @@ -219,7 +219,7 @@ class ZoomableActivity : BaseActivity() { onSwipe() } } - binding.zoomProgressBar?.let { + binding.zoomProgressBar.let { it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } } @@ -234,7 +234,7 @@ class ZoomableActivity : BaseActivity() { sharedPreferences.getBoolean(ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) if (!images.isNullOrEmpty()) { - binding.zoomable!!.setOnTouchListener( + binding.zoomable.setOnTouchListener( object : OnSwipeTouchListener(this) { // Swipe left to view next image in the folder. (if available) override fun onSwipeLeft() { @@ -271,7 +271,7 @@ class ZoomableActivity : BaseActivity() { * Handles down swipe action */ private fun onDownSwiped() { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -341,7 +341,7 @@ class ZoomableActivity : BaseActivity() { * Handles up swipe action */ private fun onUpSwiped() { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -414,7 +414,7 @@ class ZoomableActivity : BaseActivity() { * Handles right swipe action */ private fun onRightSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -451,7 +451,7 @@ class ZoomableActivity : BaseActivity() { * Handles left swipe action */ private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -646,7 +646,7 @@ class ZoomableActivity : BaseActivity() { .setProgressBarImage(ProgressBarDrawable()) .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) .build() - with(binding.zoomable!!) { + with(binding.zoomable) { setHierarchy(hierarchy) setAllowTouchInterceptionWhileZoomed(true) setIsLongpressEnabled(false) @@ -658,10 +658,10 @@ class ZoomableActivity : BaseActivity() { .setUri(imageUri) .setControllerListener(loadingListener) .build() - binding.zoomable!!.controller = controller + binding.zoomable.controller = controller if (photoBackgroundColor != null) { - binding.zoomable!!.setBackgroundColor(photoBackgroundColor!!) + binding.zoomable.setBackgroundColor(photoBackgroundColor!!) } if (!images.isNullOrEmpty()) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java index 5d480f4f73..b5f760c9f7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import fr.free.nrw.commons.R; +import java.util.Locale; public class NearbyFilterSearchRecyclerViewAdapter extends RecyclerView.Adapter @@ -121,11 +122,11 @@ protected FilterResults performFiltering(CharSequence constraint) { results.count = labels.size(); results.values = labels; } else { - constraint = constraint.toString().toLowerCase(); + constraint = constraint.toString().toLowerCase(Locale.ROOT); for (Label label : labels) { String data = label.toString(); - if (data.toLowerCase().startsWith(constraint.toString())) { + if (data.toLowerCase(Locale.ROOT).startsWith(constraint.toString())) { filteredArrayList.add(Label.fromText(label.getText())); } } 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 d238296d13..299ac4b6e1 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 @@ -87,7 +87,7 @@ class WikidataFeedback : BaseActivity() { lat, lng, ) - } as Callable>, + }, ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ aBoolean: Boolean? -> diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index 9acf5b595f..c6d09fdc66 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; /** @@ -139,14 +140,14 @@ private void setTabs() { leaderboardFragment.setArguments(leaderBoardBundle); fragmentList.add(leaderboardFragment); - titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase()); + titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); contributionsFragment = new ContributionsFragment(); Bundle contributionsListBundle = new Bundle(); contributionsListBundle.putString(KEY_USERNAME, userName); contributionsFragment.setArguments(contributionsListBundle); fragmentList.add(contributionsFragment); - titleList.add(getString(R.string.contributions_fragment).toUpperCase()); + titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java index 46ea631fb1..f44b7eb6d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java @@ -27,6 +27,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.Locale; import java.util.Objects; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; @@ -361,7 +362,7 @@ private void inflateAchievements(Achievements achievements) { + levelInfo.getMaxUniqueImages()); binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); - String levelUpInfoString = getString(R.string.level).toUpperCase(); + String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); levelUpInfoString += " " + levelInfo.getLevelNumber(); binding.achievementLevel.setText(levelUpInfoString); binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java index c4a4bf518e..cbb8c8a1cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.recentlanguages; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -117,6 +118,7 @@ public boolean findRecentLanguage(final String languageCode) { * @return Language object */ @NonNull + @SuppressLint("Range") Language fromCursor(final Cursor cursor) { // Hardcoding column positions! final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 5eb758ada8..40d743a19a 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -25,6 +25,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.Locale; import javax.inject.Inject; public class ReviewActivity extends BaseActivity { @@ -241,7 +242,7 @@ public void onDestroy() { public void showSkipImageInfo(){ DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.skip_image).toUpperCase(), + getString(R.string.skip_image).toUpperCase(Locale.ROOT), getString(R.string.skip_image_explanation), getString(android.R.string.ok), "", diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index 876fb3cd3f..c0e5097c06 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -63,7 +63,7 @@ class FailedUploadsFragment : } if (StringUtils.isEmpty(userName)) { - userName = sessionManager!!.getUserName() + userName = sessionManager.getUserName() } } @@ -96,8 +96,8 @@ class FailedUploadsFragment : fun initRecyclerView() { binding.failedUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.failedUploadsRecyclerView.adapter = adapter - pendingUploadsPresenter!!.getFailedContributions() - pendingUploadsPresenter!!.failedContributionList.observe( + pendingUploadsPresenter.getFailedContributions() + pendingUploadsPresenter.failedContributionList.observe( viewLifecycleOwner, ) { list: PagedList -> adapter.submitList(list) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index b45e4b57da..8a8fa35b32 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -19,6 +19,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import timber.log.Timber; public class FileUtils { @@ -139,7 +140,7 @@ public static String getMimeType(Context context, Uri uri) { String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri .toString()); mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - fileExtension.toLowerCase()); + fileExtension.toLowerCase(Locale.getDefault())); } return mimeType; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt index 4d79bc88e8..4442a64eaa 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt @@ -74,8 +74,8 @@ class PendingUploadsFragment : fun initRecyclerView() { binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.pendingUploadsRecyclerView.adapter = adapter - pendingUploadsPresenter!!.setup() - pendingUploadsPresenter!!.totalContributionList.observe( + pendingUploadsPresenter.setup() + pendingUploadsPresenter.totalContributionList.observe( viewLifecycleOwner, ) { list: PagedList -> contributionsSize = list.size diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java index 8503b1d05a..dd264655f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java @@ -372,7 +372,7 @@ public void onResume() { return false; }); - Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + requireView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -387,7 +387,7 @@ public void onResume() { }); Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .hide(); if (getParentFragment().getParentFragment().getParentFragment() @@ -407,7 +407,7 @@ public void onStop() { super.onStop(); if (media != null) { Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .show(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java index bd52a8d351..9000e513d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java @@ -398,7 +398,7 @@ public void onResume() { return false; }); - Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + requireView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -411,7 +411,7 @@ public void onResume() { }); Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .hide(); if (getParentFragment().getParentFragment().getParentFragment() @@ -431,7 +431,7 @@ public void onStop() { super.onStop(); if (media != null) { Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .show(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java index 2c4c2ecd3d..5581cfeb1e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java @@ -825,7 +825,7 @@ private boolean listContainsEmptyDetails(List uploadMediaDeta @Override public void displayAddLocationDialog(final Runnable onSkipClicked) { isMissingLocationDialog = true; - DialogUtil.showAlertDialog(Objects.requireNonNull(getActivity()), + DialogUtil.showAlertDialog(requireActivity(), getString(R.string.no_location_found_title), getString(R.string.no_location_found_message), getString(R.string.add_location), diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java index 7152d4d8fe..cd533401b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java @@ -129,9 +129,9 @@ public void receiveImage(final UploadableFile uploadableFile, final Place place, if (place.location != null) { final String countryCode = reverseGeoCode(place.location); if (countryCode != null && WLM_SUPPORTED_COUNTRIES - .contains(countryCode.toLowerCase())) { + .contains(countryCode.toLowerCase(Locale.ROOT))) { uploadItem.setWLMUpload(true); - uploadItem.setCountryCode(countryCode.toLowerCase()); + uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT)); } } } diff --git a/app/src/main/res/layout/activity_description_edit.xml b/app/src/main/res/layout/activity_description_edit.xml index ed50193a21..1a8d3b8cea 100644 --- a/app/src/main/res/layout/activity_description_edit.xml +++ b/app/src/main/res/layout/activity_description_edit.xml @@ -36,11 +36,11 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:contentDescription="@string/exit_location_picker" - android:tint="@color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_arrow_back_white" /> + app:srcCompat="@drawable/ic_arrow_back_white" + app:tint="@color/white" /> @@ -69,7 +69,7 @@ android:id="@+id/btn_edit_submit" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:text="@string/submit" android:textColor="@android:color/white" /> diff --git a/app/src/main/res/layout/bottom_sheet_details_explore.xml b/app/src/main/res/layout/bottom_sheet_details_explore.xml index 1da5c7f3ee..6558c9afe9 100644 --- a/app/src/main/res/layout/bottom_sheet_details_explore.xml +++ b/app/src/main/res/layout/bottom_sheet_details_explore.xml @@ -31,7 +31,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" - android:layout_marginRight="50dp" + android:layout_marginEnd="50dp" android:maxLines="2" android:ellipsize="end" /> @@ -58,6 +58,7 @@ android:layout_width="@dimen/dimen_0" android:layout_height="wrap_content" android:layout_weight="1" + android:focusable="true" android:padding="@dimen/standard_gap" android:clickable="true" android:background="@drawable/button_background_selector" @@ -69,8 +70,7 @@ android:layout_gravity="center_horizontal" android:duplicateParentState="true" app:srcCompat="@drawable/ic_directions_black_24dp" - android:tint="?attr/rowButtonColor" - /> + app:tint="?attr/rowButtonColor" /> diff --git a/app/src/main/res/layout/bottom_sheet_item_layout.xml b/app/src/main/res/layout/bottom_sheet_item_layout.xml index 4f4c2c8546..c569e523a9 100644 --- a/app/src/main/res/layout/bottom_sheet_item_layout.xml +++ b/app/src/main/res/layout/bottom_sheet_item_layout.xml @@ -1,11 +1,13 @@ @@ -14,7 +16,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:tint="?attr/rowButtonColor" /> + app:tint="?attr/rowButtonColor" /> @@ -36,7 +35,6 @@ style="?android:textAppearanceLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_horizontal" android:text="@string/level" @@ -48,13 +46,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_margin_vertical" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/black" - android:layout_marginVertical="@dimen/activity_margin_vertical" /> + android:layout_marginVertical="@dimen/activity_margin_vertical" + app:tint="@color/black" /> + android:layout_marginStart="@dimen/activity_margin_horizontal" + app:tint="@color/primaryLightColor" /> @@ -189,7 +182,6 @@ style="?android:textAppearanceMedium" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:id="@+id/images_reverted_text" android:layout_marginStart="@dimen/activity_margin_horizontal" android:text="@string/image_reverts" /> @@ -200,24 +192,19 @@ android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_toRightOf="@+id/images_reverted_text" - android:layout_toEndOf="@+id/images_reverted_text" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" android:layout_marginLeft="@dimen/activity_margin_horizontal" - android:layout_marginStart="@dimen/activity_margin_horizontal"/> + android:layout_marginStart="@dimen/activity_margin_horizontal" app:tint="@color/primaryLightColor" /> - @@ -278,7 +265,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/images_used_by_wiki_text" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/achievements_activity_margin_vertical" android:text="@string/images_used_by_wiki" /> @@ -289,12 +275,10 @@ android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_toRightOf="@+id/images_used_by_wiki_text" - android:layout_toEndOf="@+id/images_used_by_wiki_text" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" android:layout_marginLeft="@dimen/activity_margin_horizontal" - android:layout_marginStart="@dimen/activity_margin_horizontal"/> + android:layout_marginStart="@dimen/activity_margin_horizontal" + app:tint="@color/primaryLightColor" /> @@ -353,7 +337,6 @@ android:layout_height="wrap_content" android:text="@string/statistics" style="?android:textAppearanceLarge" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_vertical" android:textAllCaps="true"/> @@ -373,9 +356,7 @@ android:id="@+id/images_nearby_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/wikidata_edits" - android:layout_toLeftOf="@+id/wikidata_edits" android:orientation="horizontal" android:gravity="center_vertical"> @@ -407,14 +388,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/images_nearby_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" android:layout_gravity="top" app:layout_constraintLeft_toRightOf="@id/images_nearby_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -423,16 +403,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/wikidata_edits" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -451,9 +429,7 @@ android:id="@+id/images_featured_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/image_featured" - android:layout_toLeftOf="@+id/image_featured" android:orientation="horizontal" android:gravity="center_vertical"> @@ -486,14 +462,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/images_featured_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/images_featured_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -501,16 +476,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/image_featured" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -529,9 +502,7 @@ android:id="@+id/quality_images_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/quality_images" - android:layout_toLeftOf="@+id/quality_images" android:orientation="horizontal" android:gravity="center_vertical"> @@ -564,14 +535,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/quality_images_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/quality_images_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -579,7 +549,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" @@ -587,9 +556,8 @@ tools:text="2" android:text="0" android:id="@+id/quality_images" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -608,9 +576,7 @@ android:id="@+id/thanks_received_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/thanks_received" - android:layout_toLeftOf="@+id/thanks_received" android:orientation="horizontal" android:gravity="center_vertical"> @@ -643,14 +609,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/thanks_received_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/thanks_received_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -658,16 +623,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/thanks_received" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> diff --git a/app/src/main/res/layout/item_place.xml b/app/src/main/res/layout/item_place.xml index 82e4310631..9854fb58d4 100644 --- a/app/src/main/res/layout/item_place.xml +++ b/app/src/main/res/layout/item_place.xml @@ -11,8 +11,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/standard_gap" - android:tint="?attr/rowButtonColor" - app:srcCompat="@drawable/ic_round_star_border_24px" /> + app:srcCompat="@drawable/ic_round_star_border_24px" + app:tint="?attr/rowButtonColor" /> @@ -54,11 +52,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignTop="@id/distance" - android:layout_marginLeft="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" android:layout_toEndOf="@id/icon" - android:layout_toLeftOf="@id/distance" - android:layout_toRightOf="@id/icon" android:layout_toStartOf="@id/distance" android:ellipsize="end" android:maxLines="2" @@ -71,8 +66,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignEnd="@id/distance" - android:layout_alignLeft="@id/tvName" - android:layout_alignRight="@id/distance" android:layout_alignStart="@id/tvName" android:layout_below="@id/tvName" android:layout_marginBottom="@dimen/standard_gap" diff --git a/app/src/main/res/layout/layout_campagin.xml b/app/src/main/res/layout/layout_campagin.xml index 775a6a4ece..2a2891e84e 100644 --- a/app/src/main/res/layout/layout_campagin.xml +++ b/app/src/main/res/layout/layout_campagin.xml @@ -19,17 +19,14 @@ android:id="@+id/iv_campaign" android:layout_width="@dimen/dimen_40" android:layout_height="@dimen/dimen_40" - android:layout_marginLeft="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" android:scaleType="centerCrop" app:srcCompat="@drawable/ic_campaign" - android:tint="?attr/card_item_color" - /> + app:tint="?attr/card_item_color" /> @@ -37,15 +34,13 @@ + android:layout_marginStart="@dimen/standard_gap" + android:layout_marginEnd="@dimen/tiny_margin"> + android:visibility="visible" + app:tint="?attr/contributionsListTextSecondary" /> diff --git a/app/src/main/res/layout/nearby_card_view.xml b/app/src/main/res/layout/nearby_card_view.xml index 7161a09363..bbd43249e6 100644 --- a/app/src/main/res/layout/nearby_card_view.xml +++ b/app/src/main/res/layout/nearby_card_view.xml @@ -14,7 +14,6 @@ style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerInParent="true" android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" @@ -30,34 +29,28 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/content_layout" - android:layout_centerInParent="true" android:orientation="horizontal" > + android:id="@+id/progressBar" /> + app:tint="?attr/card_item_color" /> @@ -24,8 +25,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_photo_camera_white_24dp" /> + app:srcCompat="@drawable/ic_photo_camera_white_24dp" + app:tint="?attr/bookmarkButtonColor" /> @@ -53,8 +55,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:duplicateParentState="true" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_photo_white_24dp" /> + app:srcCompat="@drawable/ic_photo_white_24dp" + app:tint="?attr/bookmarkButtonColor" /> + android:duplicateParentState="true" + app:tint="?attr/bookmarkButtonColor" /> @@ -110,8 +114,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:duplicateParentState="true" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_overflow" /> + app:srcCompat="@drawable/ic_overflow" + app:tint="?attr/bookmarkButtonColor" /> + app:srcCompat="@drawable/ic_arrow_back_white" + app:tint="@color/white" /> \ No newline at end of file diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 9ff1b19b4c..22f382f576 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭаларҭа Иҟаҵатәуп арегистрациа Асистемахь аҭаларҭа - Шәааԥшы ԥыҭрак... + Шәааԥшы ԥыҭрак… Аҭалара қәҿиарала имҩаԥысит! Асистемахь аҭалараан агха! Афаил ԥшаам. Даҽа фаилк шәахәаԥш. @@ -64,7 +64,7 @@ Ари шәара еилышәкаама? Ааи! Акатегориақәа - Аҭагалара... + Аҭагалара… Акагь алхӡам Иҟам ахҳәаа Идырым алицензиа diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 1da8b3101f..57ba77cc93 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -22,7 +22,6 @@ %1$d lêers aan die uploaden - \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -148,7 +147,7 @@ Ja! <u>Meer inligting</u> Kategorieë - Laai ... + Laai … Niks gekies nie Geen beskrywing Geen bespreking nie diff --git a/app/src/main/res/values-anp/strings.xml b/app/src/main/res/values-anp/strings.xml index 70a01949f2..e4029af9b0 100644 --- a/app/src/main/res/values-anp/strings.xml +++ b/app/src/main/res/values-anp/strings.xml @@ -27,14 +27,14 @@ पासवर्ड भूलाय गेलौ की? साइन अप करौ प्रवेश होय रहलौ छौं - कृपया प्रतीक्षा करौ... - कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ… प्रवेश विफल अपलोड आरंभ! हाल केरौ अपलोड कतारबद्ध विफल - अपलोड होय रहलौ छौं... + अपलोड होय रहलौ छौं… ठामे मँ हमरौ अपलोड साझा करौ @@ -68,7 +68,7 @@ हाँव! बेसी जानकारी श्रेणी सिनी - लोड होय रहलौ छौं... + लोड होय रहलौ छौं… कुछु चयनित नाय कोय शीर्षक नाय कोय विवरण नाय @@ -173,7 +173,7 @@ पूर्ण होलौं अगलका छवि हाँव, केन्हअ नाय - कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ… प्रतिलिपि बनैलौ गेलै! लेखक स्थान diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 46ffcb7f56..b0fda69907 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار... + يرجى الانتظار… نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -530,7 +530,7 @@ عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار... + الرجاء الانتظار… الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +552,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر... + مشاركة التطبيق عبر… معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -695,7 +695,7 @@ وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل ... + جاري استئناف التحميل … جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 960b55bdad..63aa12b964 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -27,7 +27,7 @@ পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক লগইন হৈ আছে - অনুগ্ৰহ কৰি অপেক্ষা কৰক... + অনুগ্ৰহ কৰি অপেক্ষা কৰক… লগইন সফল হ\'ল! লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। @@ -74,7 +74,7 @@ <u>গোপনিয়তা নীতি</u> প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই - প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত… আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index df61ed0610..34212aebbc 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -74,7 +74,7 @@ Aniciando sesión Espera… Actualizando pies y descripciones - Porfavor espera... + Porfavor espera… ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. @@ -480,7 +480,7 @@ Númberos de serie Software Xubi semeyes a Wikimedia Commons direutamente dende\'l to móvil. Descarga yá la app de Commons: %1$s - Compartir l\'aplicación per... + Compartir l\'aplicación per… Información de la imaxe Nun s\'alcontró nenguna categoría Nun s\'alcontraron retratos diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 1edbe43fce..d2ea468ad7 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -104,7 +104,7 @@ CC BY 3.0 Əlavə məlumat Kateqoriyalar - Yüklənir... + Yüklənir… Heç biri seçilməmişdir Naməlum lisenziya Yenilə diff --git a/app/src/main/res/values-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 4fa660ef88..8e77643235 100644 --- a/app/src/main/res/values-b+roa+tara/strings.xml +++ b/app/src/main/res/values-b+roa+tara/strings.xml @@ -40,8 +40,8 @@ Tràse Passuord scurdate? Reggistrate - Stoche a tràse... - Aspitte... + Stoche a tràse… + Aspitte… E\' trasute! Non g\'è trasute! File non acchiate. Pruève \'n\'otre file. @@ -121,7 +121,7 @@ Permesse richieste Non ge tìne notifeche non lette Errore assute mendre ca ste pigghiave le immaggine - Aspitte... + Aspitte… Zumbe ste immaggine Autore Lènghe d\'a descrizione predefinite diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index cd1cb09e8f..b8b602d0dc 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -5,7 +5,7 @@ * Milicevic01 * Zoranzoki21 --> - + Fejsbuk stranica Ostave Izvorni kod na Github-u Logo Ostave @@ -26,32 +26,39 @@ Slika dana %1$d datoteka se otprema + %1$d datoteke se otpremaju %1$d datoteke se otpremaju %1$d otpremanje + %1$d otpremanja %1$d otpremanja Pokretanje otpremanja Procesuiranje %d otpremanje + Procesuiranje %d otpremanja Procesuiranje %d otpremanja %d otpremanje + %d otpremanja %d otpremanja Slika će se voditi pod licencom %1$s + Slike će se voditi pod licencom %1$s Slike će se voditi pod licencom %1$s %1$d otpremanje + %1$d otpremanja %1$d otpremanja - Primanje deljenog sadržaja... Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja - Primanje deljenog sadržaja... Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja Istraga Izgled @@ -486,7 +493,7 @@ Pristup lokaciji medija je odbijen Možda nećemo moći da automatski pribavimo podatke o lokaciji iz slika koje otpremite. Dodajte odgovarajuću lokaciju za svaku sliku pre objavljivanja Otpremi fotografije na Vikimedijinu Ostavu direktno sa svog telefona. Preuzmi aplikaciju Ostave sada: %1$s - Podeli aplikaciju preko... + Podeli aplikaciju preko… Informacije o slici Nisu pronađene kategorije Otkazano otpremanje @@ -511,12 +518,13 @@ Uspešno Kategorija %1$s je dodata. + Kategorije %1$s su dodate. Kategorije %1$s su dodate. Nije moguće dodati kategorije. Ažuriraj kategoriju Uredi prikaze - Pokušavanje promena koordinata... + Pokušavanje promena koordinata… Ažuriranje koordinata Ažuriranje opisa Ažuriranje natpisa @@ -698,6 +706,7 @@ Nije moguće podeliti ovu stavku %d slika je odabrana + %d slika je odabrano %d slika je odabrano diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 0fc68329f1..4c33b396fe 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -61,9 +61,9 @@ Серһүҙҙе оноттоғоҙмо? Теркәлеү Системаға инеү - Зинһар, көтөгөҙ... + Зинһар, көтөгөҙ… Аңлатмалар һәм тасуирламалар яңыртыла - Зинһар, көтөгөҙ... + Зинһар, көтөгөҙ… Системаға инеү уңышлы! Системаға инеү уңышһыҙ! Файл табылманы. Башҡа файлды эҙләп ҡарағыҙ. @@ -131,7 +131,7 @@ Фекереңде ебәр (эл.почта аша) Почта клиенты асыҡланмаған Яңыраҡ ҡулланылған категориялар - Тәүге синхронлаштырыуҙы көтөү... + Тәүге синхронлаштырыуҙы көтөү… Әлегә бер фото ла йөкләмәгәнһегеҙ Ҡабатларға Кире алыу @@ -171,7 +171,7 @@ Эйе! Ентеклерәк Категориялар - Йөкләнә башланы... + Йөкләнә башланы… Бер ни ҙә һайланмаған Тасуирламаһы юҡ Фекер алышыу юҡ diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index b24bc00221..1273eaf0d6 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -61,7 +61,7 @@ Lali kruna Sandi? Daftar Ngeranjingin log - Jantos dumun... + Jantos dumun… Nganyarin sesirah miwah pidarta Jantos dumun… Mahasil manjing log! @@ -303,7 +303,7 @@ Nomor seri Piranti lunak Unggah foto nuju Wikimédia Commons langsung saking télépon ragané. Unduh aplikasi Commons mangkin: %1$s - Wedar aplikasi saking... + Wedar aplikasi saking… Pidarta Gambar Pangunggahan Kawangdé %1$s kaunggah olih: %2$s @@ -340,7 +340,7 @@ Kaanggén Paringkat Titiang Kualitas Gambar - Ngalanturang unggahan... + Ngalanturang unggahan… Ngarérénang unggahan… Wangdé Unggah Lisénsi Média diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 6ee9315423..cb19d6e39f 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -304,7 +304,7 @@ Преглеждане на прочетени Преглеждане на непрочетени Възникна грешка при избирането на изображенията - Моля, изчакайте... + Моля, изчакайте… напълно размазано Наблизо Прочетете повече diff --git a/app/src/main/res/values-blk/strings.xml b/app/src/main/res/values-blk/strings.xml index 2cf4aba5bd..51a8ec1ba8 100644 --- a/app/src/main/res/values-blk/strings.xml +++ b/app/src/main/res/values-blk/strings.xml @@ -38,7 +38,7 @@ အွောန်ႏဖေင်ꩻထိုꩻ ငဝ်းဗိဉ်ႏပလို့ꩻနဲ့? ဒင်ႏမတ်ပိုင်တိဉ် အဝ်ႏနွို့အကောက်ကျာꩻ - အိုင်ပွေားဆောင်းတဆင်ႏသြ... + အိုင်ပွေားဆောင်းတဆင်ႏသြ… နွို့အကောက်အောင်ႏလဲဉ်း! နွို့အကောက်အောင်ႏတဝ်း! မော့ꩻတဝ်းဖုဲင်၊ စံꩻထွားစံꩻသွော့ ဖုဲင်အလင်တဗာႏသြ။ @@ -97,7 +97,7 @@ မွေး! ထဲင်းယင်း သꩻတင်ꩻအချက်လက် ကဏ္ဍဖုံႏ - အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ... + အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ… လွိုက်ခါꩻတဝ်းမုဲင်ꩻမုဲင်ꩻ ပုင်ႏလိတ်အဝ်ႏတဝ်း အွောန်ႏနယ်ချက်အဝ်ႏတဝ်း diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 2d156c1999..51502c2649 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -393,7 +393,7 @@ কোনও চিত্র ব্যবহৃত হয়নি পঠিতগুলি দেখান অপঠিতগুলি দেখান - অনুগ্রহ করে অপেক্ষা করুন... + অনুগ্রহ করে অপেক্ষা করুন… অনুলিপি করা হয়েছে এই চিত্র এড়িয়ে যান প্রণেতা diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 1c7d09617a..9537c45e62 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -40,6 +40,9 @@ %1$d bellgargadenn loc\'het + %1$d bellgargadenn loc\'het + %1$d bellgargadennoù loc\'het + %1$d bellgargadennoù loc\'het %1$d pellgargadennoù loc\'het @@ -51,6 +54,9 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ + gant an aotre-implijout %1$s e vo an div skeudenn-mañ + gant an aotre-implijout %1$s e vo meur a skeudenn-mañ + gant an aotre-implijout %1$s e vo kalz a skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ Ergerzhout diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 91860b1e12..d178ff507a 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -10,19 +10,22 @@ Logo Commonsa postavlja se %1$d datoteka + postavlja se %1$d datoteke postavlja se %1$d datoteka - \@string/contributions_subtitle_zero postavljena %1$d datoteka + postavljena %1$d datoteke postavljenih datoteka: %1$d Započinjem postavljanje %1$d datoteke + Započinjem postavljanje %1$d datoteke Započinjem postavljanje %1$d datoteka/-e %1$d postavljanje + %1$d postavljanja %1$d postavljanja Slika će se voditi pod licencom %1$s diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 0c15e58c34..51330cb9d2 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,27 +20,33 @@ Imatge del dia s\'està carregant %1$d fitxer + S\'estan carregant de %1$d fitxers s\'estan carregant %1$d fitxers (%1$d) + (%1$d) (%1$d) S\'inicien les càrregues S\'està processant %1$d càrrega + S\'estan processant %1$d càrregues S\'estan processant %1$d càrregues %d càrrega + $d càrregues %d càrregues Aquesta imatge quedarà sota llicència %1$s + Aquestes imatges quedaran sota llicència %1$s Aquestes imatges quedaran sota llicència %1$s %1$d pujada + %1$d pujades %1$d pujades Explora @@ -392,7 +398,7 @@ Model de lent Números de sèrie Programari - Comparteix l\'aplicació a través de... + Comparteix l\'aplicació a través de… Informació de la imatge No s’ha trobat cap categoria No s\'han trobat representacions diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index e13e8c040d..e25b83e259 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -64,7 +64,7 @@ Викиларма Параметраш Викиларма чуйаккха - ДӀадоьдуш ду чуйаккхар... + ДӀадоьдуш ду чуйаккхар… Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо @@ -146,7 +146,7 @@ ЦӀе: Сиднейн операн театр ХӀаъ! Категореш - Чуйолуш... + Чуйолуш… ХӀума хаьржина йац Куьг доцуш Хаамаш бац @@ -297,7 +297,7 @@ Серийн лоьмар Программан кхачам Файл йолу меттиган тӀекхача бакъо ца ло - Йекъа программа, гӀоьнца... + Йекъа программа, гӀоьнца… Суьртан информаци Цхьа а категори ца карийна. Цхьа а хаам ца карийна. @@ -362,7 +362,7 @@ ДӀайаьккхина закладки йукъара Цхьа хӀума галдаьлла. Фонан сурт хӀотто аьтто ца баьлла Фонан сурт санна хӀоттайе - Фонан сурт дӀахӀоттош ду... + Фонан сурт дӀахӀоттош ду… Системин нисдаран гӀирс Бодане Сирла diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4d49ee6327..fb4ee05ef3 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,31 +32,44 @@ Obrázek dne %1$d soubor se nahrává + %1$d soubory se nahrávají + %1$d souborů se nahrává %1$d souborů se nahrává - \@string/contributions_subtitle_zero (%1$d) + (%1$d) + (%1$d) (%1$d) Spouští se nahrávání %1$d souboru + Spouští se nahrávání %1$d souborů + Spouští se nahrávání %1$d souborů Spouští se nahrávání %1$d souborů %1$d nahrávání + %1$d nahrávání + %1$d nahrávání %1$d nahrávání Tento obrázek bude zveřejněn pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s %1$d nahrání + %1$d nahrávání + %1$d nahrávání %1$d nahrání Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení + Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení + Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Objevit diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 623a48c8c2..7cdfb13825 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -29,7 +29,7 @@ Wlogùjë mie Wregistrëjë sã Logòwanié - Proszã żdac... + Proszã żdac… Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. @@ -78,7 +78,7 @@ Sélôj òpinijã (przez e-mail) Felënk wjinstalowónegò e-mailowégò klienta Slédno ùżëwóne kategòrëje - Żdanié na pierszą synchronizacëjã... + Żdanié na pierszą synchronizacëjã… Nie môsz jesz wladowónych òdjimków Próbùjë znowa Òprzestóń @@ -99,7 +99,7 @@ Przëmiôr wladënka: Jo! Kategòrëje - Wladënk... + Wladënk… Felënk nacéchòwaniô Felënk òpisënka Nieznónô licencëja diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 8c4b4a652a..50df9b6d8c 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -21,25 +21,44 @@ Popeth Llun y Dydd + %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho \@string/contributions_subtitle_zero (%1$d) + (%1$d) + (%1$d) + (%1$d) (%1$d) Cychwyn Uwchlwytho + Dechrau %1$d uwchlwythiad Cychwyn %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad Cychwyn uwchlwytho %1$d ffeil + %1$d uwchlwythiad %1$d uwchlwythiad + %1$d uwchlwythiad + %1$d uwchlwythiad + %1$d uwchlwythiad %1$d uwchlwythiad + Ni chaiff unrhyw ddelweddau eu trwyddedu dan %1$s Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s Caiff y delweddau hyn eu trwyddedu dan %1$s Archwilio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 3b6822c47b..80a82afb54 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,7 @@ Adgang til medieplacering nægtet Vi kan muligvis ikke automatisk indhente placeringsdata fra billeder, du uploader. Tilføj den passende placering for hvert billede, før du indsender Upload billeder til Wikimedia Commons direkte fra din telefon. Download Commons-appen nu: %1$s - Del app via... + Del app via… Billedoplysninger Ingen kategorier blev fundet Ingen afbildninger fundet @@ -642,9 +642,9 @@ Begrænset forbindelsestilstand Kvalitetsbilleder Kvalitetsbilleder er tegninger eller fotografier, der opfylder visse kvalitetsstandarder (som for det meste er af teknisk karakter) og er værdifulde for Wikimedia-projekter - Genoptager upload... - Sætter upload på pause... - Annullerer upload... + Genoptager upload… + Sætter upload på pause… + Annullerer upload… Annuller upload Du har aktiveret begrænset forbindelsestilstand. Alle uploads er sat på pause og genoptages, når du deaktiverer denne tilstand. Begrænset forbindelsestilstand aktiveret! @@ -784,7 +784,7 @@ Andet problem eller anden information (forklar venligst nedenfor). Din feedback bliver slået op på følgende wiki-side: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Er du sikker på, at du vil annullere alle uploads? - Annullerer alle uploads... + Annullerer alle uploads… Uploads Afventer Mislykkedes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a6471c1fe9..4043040214 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -516,7 +516,7 @@ Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten Bitte warten … - Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. + Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. Über Orte in der Nähe hochgeladene Bilder sind die Bilder, die von entdeckten Orten auf der Karte hochgeladen wurden. Diese Funktion erlaubt es Autoren, eine Dankeschön-Benachrichtigung an Benutzer zu senden, die nützliche Bearbeitungen durchgeführt haben – durch die Benutzung eines kleinen Dankeschön-Links in der Versionsgeschichte oder Unterschiedsseite. Auf Folgemedien kopieren @@ -611,7 +611,7 @@ zu den Lesezeichen hinzugefügt Etwas ist schiefgelaufen. Das Hintergrundbild konnte nicht eingestellt werden Als Hintergrundbild festlegen - Hintergrundbild wird festgelegt. Bitte warten... + Hintergrundbild wird festgelegt. Bitte warten… Systemeinstellung Dunkel Hell diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index 840b0198dc..ebb3cfbe4e 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -62,8 +62,8 @@ Parola, xo vira kerde? Qeyd be Kewno cı - Kerem kerên, bıpawên... - Kerem ke, bıpawe... + Kerem kerên, bıpawên… + Kerem ke, bıpawe… Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. @@ -93,7 +93,7 @@ Şınasnayış Bınnuşte Xırabiya kewten-network xeta - Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. + Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2–3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Kodê kamiya raştkerdışi dıfaktorın gani cı kewê. Nidekeweya de @@ -298,7 +298,7 @@ Pêhesnayışê toyê wendışi çıniyê Wendışi bıvêne Nêwendeyan bıvêne - Kerem kerên, bıpawên... + Kerem kerên, bıpawên… Nê resımi raviyarnê Nuştekar Heqa telifi diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e3675ef0a3..e4b597fb12 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Σύνδεση Ξεχάσατε τον κωδικό πρόσβασης σας; Εγγραφή - Γίνεται σύνδεση... + Γίνεται σύνδεση… Παρακαλούμε αναμείνετε… Ενημέρωση λεζάντων και περιγραφών Παρακαλούμε αναμείνετε… @@ -204,7 +204,7 @@ Ναι! Περισσότερες πληροφορίες Κατηγορίες - Φόρτωση σε εξέλιξη... + Φόρτωση σε εξέλιξη… Καμία επιλεγμένη Χωρίς λεζάντα Χωρίς περιγραφή @@ -521,7 +521,7 @@ Δεν επιτρέπεται η πρόσβαση στην τοποθεσία πολυμέσων Ενδέχεται να μην μπορούμε να λάβουμε αυτόματα δεδομένα τοποθεσίας από φωτογραφίες που ανεβάζετε. Προσθέστε την κατάλληλη τοποθεσία για κάθε εικόνα πριν την υποβολή Ανεβάστε φωτογραφίες στα Wikimedia Commons απευθείας από το τηλέφωνό σας. Κάντε λήψη της εφαρμογής Commons τώρα: %1$s - Κοινή χρήση εφαρμογής μέσω... + Κοινή χρήση εφαρμογής μέσω… Πληροφορίες Εικόνας Δεν βρέθηκαν Κατηγορίες Δεν βρέθηκαν απεικονίσεις @@ -798,7 +798,7 @@ Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω). Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a> Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις; - Ακύρωση όλων των μεταφορτώσεων... + Ακύρωση όλων των μεταφορτώσεων… Μεταφορτώσεις Σε εκκρεμότητα Απέτυχε diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 69673afbed..323c823b25 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -78,7 +78,7 @@ Ĉu pasvorto forgesita? Registriĝi Ensalutado - Bonvolu atendi... + Bonvolu atendi… Ĝisdatiganta subtekstojn kaj priskribojn Bonvolu atendi… Ensalutado sukcesis @@ -150,7 +150,7 @@ Sendi viajn komentojn (per retpoŝto) Neniu retpoŝtilo instalita Laste uzitaj kategorioj - Atendas la unuan Sinkronigado... + Atendas la unuan Sinkronigado… Vi ankoraŭ ne alŝutis fotojn. Reprovi Nuligi @@ -190,7 +190,7 @@ Jes! <u>Ekscii pli</u> Kategorioj - Ŝargado... + Ŝargado… Neniu elektita Neniu substeksto Sen priskribo @@ -482,7 +482,7 @@ Vidu legitajn Vidi nelegitojn Eraro okazis dum elektado de bildoj - Bonvolu atendi... + Bonvolu atendi… Elstaraj bildoj estas tiuj bildoj far tre spertaj fotografistoj kaj ilustristoj, kiujn la komunumo de Vikimedia Komunejo elektis kiel iujn de la plej alta kvalito en la retejo. Bildoj Alŝutitaj per Apudaj lokoj estas bildoj alŝutitaj per trovado de lokoj sur la mapo. Tiu funkcio ebligas sendi Dankantan sciigon al farinto de utila redakto – per malgranda dankiga ligilo ĉe la paĝo de historio aŭ diferenco. @@ -504,7 +504,7 @@ Aliro al loko de plurmediaĵo malakceptita Ni eble ne povos aŭtomate akiri pri-lokajn datumojn de bildoj, kiujn vi alŝutas. Bonvolu aldoni la taŭgan lokon por ĉiu bildo antaŭ ol sendi Alŝutu fotojn al Vikimedia Komunejo rekte de via telefono. Elŝutu la Komunejan aplikaĵon nun: %1$s - Diskonigi aplikaĵon per... + Diskonigi aplikaĵon per… Informo pri Bildo Neniu Kategorio troviĝis Neniu bildo-priskribo trovita @@ -636,9 +636,9 @@ Modo por limigita konekto Kvalitaj Bildoj Kvalitaj bildoj estas diagramoj aŭ fotoj kiuj kontentigas certajn normojn pri kvalito (kiuj estas plejparte teknikaj) kaj estas valoraj por Vikimediaj projektoj. - Rekomencante alŝuton... - Paŭzante alŝuton... - Nuligante alŝuton... + Rekomencante alŝuton… + Paŭzante alŝuton… + Nuligante alŝuton… Ĉesigi alŝutadon Vi aktivigis Modon por limigita konekto. Ĉiuj alŝutoj estas paŭzitaj kaj rekomencos post kiam vi malŝaltos ĉi modon. Modo por limigita konekto estas aktivigita. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4e90f68641..2189534705 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,7 +51,7 @@ * Vivaelcelta * Wizardeck --> - + Página de Facebook de Commons Código fuente de Commons en GitHub Logo de Commons @@ -75,31 +75,38 @@ Foto del día Cargando %1$d archivo + Cargando %1$d archivos Cargando %1$d archivos (%1$d) + (%1$d) (%1$d) Comenzando las subidas Procesando %d carga + Procesando %d cargas Procesando %d cargas %d carga + %1 cargas %1 cargas Esta imagen se publicará bajo la licencia %1$s + Estas imágenes se publicarán bajo la licencia %1$s Estas imágenes se publicarán bajo la licencia %1$s %1$d Subida + %1$d Subidas %1$d Subidas Recepción de contenido compartido. El procesamiento de la imagen puede tardar cierto tiempo, dependiendo del tamaño de la imagen y de tu dispositivo + Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Explorar @@ -335,7 +342,7 @@ Omitir tutorial Internet no disponible Error al recuperar las notificaciones - Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. + Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. No se encontró ninguna notificación Traducir Idiomas @@ -477,7 +484,7 @@ Permitir Descartar Por favor, activa el acceso a la ubicación desde Configuración y vuelva a intentarlo. \n\nNota: Es posible que la subida no tenga datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. - La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. + La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. La aplicación no registrará la ubicación junto con las tomas debido a la falta del permiso de la ubicación. La aplicación no registrará la ubicación junto con las tomas porque el GPS está apagado Utilizar el selector de fotografías basado en documentos @@ -505,8 +512,8 @@ ¿Está correctamente categorizado? ¿Está dentro de los objetivos del proyecto? ¿Quieres agradecer al colaborador? - Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. - Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado + Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. + Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado Tu apreciación animara a %1$s ¡Oh, esto ni siquiera esta categorizado! Esta imagen esta dentro de %1$s categorías. @@ -524,7 +531,7 @@ Compartir registros usando Ver leídas Ver no leidas - Ocurrió un error mientras se elegían imagenes + Ocurrió un error mientras se elegían imágenes Un momento… Las imágenes destacadas son creaciones de talentosos fotógrafos e ilustradores que la comunidad de Wikimedia Commons ha reconocido como las de mayor calidad del sitio. Las imágenes subidas vía Lugares Cercanos son las imágenes que han sido subidas al descubrir lugares en el mapa. @@ -547,7 +554,7 @@ Acceso a la ubicación del archivo multimedia denegado Es posible que no podamos obtener automáticamente los datos de ubicación de las imágenes que suba. Añada la ubicación adecuada a cada imagen antes de enviarla Sube fotos a Wikimedia Commons directamente desde tu celular. Descarga la aplicación de Commons ahora: %1$s - Compartir la aplicación vía... + Compartir la aplicación vía… Información de la imagen No se encontró ninguna categoría No se encontraron representaciones @@ -574,6 +581,7 @@ Éxito Se añade %1$s categoría. + Se añaden %1$s categorías. Se añaden %1$s categorías. No se pudieron añadir las categorías. @@ -582,6 +590,7 @@ Editar las descripciones %1$s Se añade la descripción. + Descripción %1$s se añadieron. Descripción %1$s se añadieron. No se pueden añadir descripciones. @@ -599,7 +608,7 @@ Las coordenadas de la imagen no están actualizadas. No se puede obtener descripciones. Editar descripciones y leyendas - Compartir imagen via + Compartir imagen via Todavía no has hecho ninguna contribución. %s Aún no ha realizado ninguna contribución Cuenta creada @@ -624,7 +633,7 @@ añadido a marcadores Algo salió mal. No se pudo establecer el fondo de pantalla Colocar como fondo de pantalla - Estableciendo el fondo de pantalla. Por favor espere... + Estableciendo el fondo de pantalla. Por favor espere… Seguir sistema Oscuro Claro @@ -682,9 +691,9 @@ Modo de conexión limitada Imágenes de calidad Las imágenes de calidad son diagramas o fotografías que cumplen determinados estándares de calidad (mayormente de carácter técnico) y que son valiosas para proyectos de Wikimedia - Reanudando carga... - Pausando carga... - Cancelando carga... + Reanudando carga… + Pausando carga… + Cancelando carga… Cancelar carga Has habilitado el modo de conexión limitada. Todas las cargas están pausadas y se reanudarán cuando deshabilites este modo. El modo de conexión limitada está encendido. @@ -811,7 +820,8 @@ Guardar archivo GPX %d imagen seleccionada - %d imagenes seleccionadas + %d imágenes seleccionadas + %d imágenes seleccionadas Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado. Nota sobre cargas múltiples @@ -819,7 +829,7 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. - Cancelando todas las subidas... + Cancelando todas las subidas… Subidas Pendiente Falló diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index ff75cbc7f6..3dd463b345 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: Naturako elementuak (loreak, animaliak, mendiak) Objektu erabilgarriak (bizikletak, tren geltokiak) - Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat…) Mesedez EZ igo: Autorretratuak edo zure lagunen argazkiak Internetetik jaitsitako irudiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 841160581f..4cdd2b87a9 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -83,7 +83,7 @@ رمز عبور خودتان را فراموش کرده‌اید؟ ثبت نام واردشدن - شکیبا باشید... + شکیبا باشید… ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. @@ -122,7 +122,7 @@ تغییرها بارگذاری جستجوی رده‌ها - جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، ...) + جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، …) ذخیره تازه کردن فهرست @@ -411,7 +411,7 @@ شما هیچ اعلان خوانده‌شده‌ای ندارید نمایش دیده‌شده مشاهده خوانده نشده ها - لطفاً صبر کنید... + لطفاً صبر کنید… نمونه تصاویری که برای بازگذاری مناسب نیستند از این تصویر صرف نظر کن مدیریت تگ‌های EXIF @@ -423,7 +423,7 @@ مدل لنز شماره سریال نرم‌افزار - اشتراک از طریق... + اشتراک از طریق… اطلاعات عکس هیچ رده‌ای یافت نشد بارگذاری لغو شد @@ -455,7 +455,7 @@ به بوکمارک‌ها افزوده شد مشکل به وجود آمد. به عنوان پس‌زمینه انتخاب نشد. انتخاب به عنوان پس‌زمینه - قرار دادن پس‌زمینه. لطفاً صبر کنید... + قرار دادن پس‌زمینه. لطفاً صبر کنید… سامانه را دنبال کنید تیره روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 312ebc84c5..26328a3e2c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -80,7 +80,7 @@ Kirjaudutaan Odota… Päivitetään kuvatekstejä ja kuvauksia - Odota... + Odota… Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. @@ -481,7 +481,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s - Jaa sovellus... + Jaa sovellus… Kuvan tiedot Luokkia ei löytynyt Kuvauksia ei löytynyt @@ -546,7 +546,7 @@ Lisätty kirjanmerkkeihin Jotain meni väärin. Ei voitu asettaa taustakuvaksi. Aseta taustakuvaksi - Asetetaan taustakuvaksi. Odota... + Asetetaan taustakuvaksi. Odota… Käytä järjestelmän Tumma Vaalea @@ -594,8 +594,8 @@ Rajoitettu yhteistila pois päältä. Jonossa olevat lähetykset kopioidaan nyt. Rajoitettu yhteystila Laatukuvat - Jatketaan lähettämistä... - Keskeytetään lähetys... + Jatketaan lähettämistä… + Keskeytetään lähetys… Peruutetaan tallennusta… Peruuta tallennus Rajoitettu yhteystila on päällä. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 992f418af0..995e4041b3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -46,7 +46,7 @@ * Wladek92 * Y-M D --> - + Page Facebook de Commons Code source Github de Commons Logo de Commons @@ -70,31 +70,38 @@ Image du jour %1$d fichier en cours de téléversement + %1$d fichiers en cours de téléversement %1$d fichiers en cours de téléversement (%1$d) + (%1$d) (%1$d) Démarrage des téléversements %d téléversement en cours + %d téléversements en cours %d téléversements en cours %d téléversement + %d téléversements %d téléversements Cette image sera sous licence %1$s. + Ces images seront sous licence %1$s. Ces images seront sous licence %1$s. %1$d téléversement + %1$d téléversements %1$d téléversements - Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. + Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. + Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. Explorer @@ -113,9 +120,9 @@ Mot de passe oublié ? S’inscrire Connexion - Veuillez patienter... + Veuillez patienter… Mise à jour des légendes et des descriptions - Veuillez patienter... + Veuillez patienter… Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. @@ -185,7 +192,7 @@ Envoyer vos commentaires (par courriel) Aucun client de courriel installé Catégories récemment utilisées - En attente de première synchronisation... + En attente de première synchronisation… Vous n’avez encore téléchargé aucune photo. Réessayer Annuler @@ -225,7 +232,7 @@ Oui ! Davantage d’informations Catégories - Chargement en cours... + Chargement en cours… Aucune catégorie sélectionnée Aucune légende Aucune description @@ -521,7 +528,7 @@ Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images - Veuillez patienter... + Veuillez patienter… Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisies comme étant de la meilleure qualité pour le site. Les images téléversées par « Lieux à proximité » sont les images téléversées lors de la découverte de lieux sur la carte. Cette fonctionnalité permet aux contributeurs d’envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles ― en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff. @@ -543,7 +550,7 @@ Accès à l’emplacement du média refusé Nous ne pourrons pas obtenir automatiquement les données de localisation des images que vous téléchargez. Veuillez ajouter l’emplacement approprié pour chaque image avant de la soumettre. Téléversez des photos sur Wikimedia Commons directement depuis votre téléphone. Téléchargez l’application Commons maintenant : %1$s - Partager l’application via... + Partager l’application via… Informations sur l’image Aucune catégorie trouvée Aucun élément représenté trouvé @@ -570,6 +577,7 @@ Succès La catégorie %1$s est ajoutée. + Les catégories %1$s sont ajoutées. Les catégories %1$s sont ajoutées. Impossible d’ajouter des catégories. @@ -578,6 +586,7 @@ Modifier les éléments représentés L’élément représenté %1$s est ajouté. + Les éléments représentés %1$s sont ajoutés. Les éléments représentés %1$s sont ajoutés. Impossible d’ajouter des éléments représentés. @@ -620,7 +629,7 @@ Ajouté aux favoris Un problème est survenu. Impossible d’installer le fond d’écran. Définir comme fond d’écran - Installation du fond d’écran. Veuillez patienter... + Installation du fond d’écran. Veuillez patienter… Suivre le système Sombre Clair @@ -678,9 +687,9 @@ Mode de connexion limitée Images de qualité Les images de qualité sont des diagrammes ou des photographies qui respectent certains standards de qualité (qui sont, par nature, essentiellement techniques) et sont précieuses pour les projets Wikimedia. - Reprise du téléversement... - Mise en pause du téléversement... - Annulation du téléversement... + Reprise du téléversement… + Mise en pause du téléversement… + Annulation du téléversement… Annuler le téléversement Vous avez activé le mode de connexion limitée. Tous les téléversements sont suspendus et reprendront une fois ce mode désactivé. Le mode de connexion limitée est actif. @@ -809,6 +818,7 @@ Fichier GPX enregistré %d image sélectionnée + %d images sélectionnées %d images sélectionnées Souvenez-vous que toutes les images dans une importation multiple prennent les mêmes catégories et descriptions. Si les images de partagent pas les descriptions et catégories, veuillez effectuer plusieurs importations séparées. @@ -822,7 +832,7 @@ Autre problème ou information (merci d\'expliquer ci-dessous). Vos commentaires sont publiés sur la page wiki suivante : <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Êtes-vous sûr de vouloir annuler tous les téléchargements ? - Annulation de tous les téléchargements... + Annulation de tous les téléchargements… Téléversements En attente Échec diff --git a/app/src/main/res/values-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index b0ec664235..4659eecf1a 100644 --- a/app/src/main/res/values-gcr/strings.xml +++ b/app/src/main/res/values-gcr/strings.xml @@ -38,9 +38,9 @@ Ou bliyé ou Kodsigré ? Enskri oukò Konnègsyon - Souplé antann... + Souplé antann… Mizajou di léjann-yan ké dèskripsyon-yan - Souplé antann... + Souplé antann… Konnègsyon bon ! Konnègsyon pabon ! Fiché pa trouvé. Souplé éséyé ké rounòt. @@ -96,7 +96,7 @@ Enren ! Plis lenfòrmasyon Katégori-ya - Chajman ka fèt... + Chajman ka fèt… Pyès katégori sélègsyonnen Pyès léjann Pyès dèskripsyon diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1740c1890c..e11716a514 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -452,7 +452,7 @@ Modelo de lente Números de serie Software - Compartir a aplicación vía... + Compartir a aplicación vía… Información da imaxe Non se atoparon categorías Cancelouse a carga diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 50a04319b9..2375838538 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -39,7 +39,6 @@ %1$d फ़ाइलें अपलोड हो रहीं - \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -70,8 +69,8 @@ पासवर्ड भूल गये? खाता बनायें लॉग इन हो रहा है - कृपया प्रतीक्षा करें... - कृपया प्रतीक्षा करें... + कृपया प्रतीक्षा करें… + कृपया प्रतीक्षा करें… लॉग इन सफल! लॉग इन विफल! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। @@ -350,7 +349,7 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं... + सभी अपलोड रद्द किये जा रहे हैं… अपलोड लंबित विफल हुआ diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index d2d731c392..414f0dd40a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -15,19 +15,22 @@ Slika dana Postavlja se %1$d datoteka + Postavlja se %1$d datoteke Postavljaju se %1$d datoteke - \@string/contributions_subtitle_zero %1$d postavljena datoteka + %1$d postavljena datoteke %1$d postavljene datoteke Započeto %1$d postavljanje + Započinjem %1$d postavljanja Započeta %1$d postavljanja %1$d postavljanje + %1$d postavljanja %1$d postavljanja Ova će slika biti licencirana pod %1$s @@ -46,7 +49,7 @@ Zaboravljena zaporka? Otvori račun Prijava - Molimo pričekajte ... + Molimo pričekajte … Prijava uspješna! Prijava neuspješna! Datoteka nije pronađena. Molimo probajte drugu. @@ -104,7 +107,7 @@ Pošaljite povratnu informaciju (putem elektroničke pošte) Klijent za elektroničku poštu nije instaliran Nedavno rabljene kategorije - Pričekajte za prvu sinkronizaciju... + Pričekajte za prvu sinkronizaciju… Nemate još postavljenih slika. Pokušaj ponovo Odustani @@ -144,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje... + Učitavanje… Ništa nije odabrano Nema opisa Nepoznata licencija @@ -193,7 +196,7 @@ Stranica datoteke na Zajedničkom poslužitelju Stavka na Wikidati Članak na Wikipediji - Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,… Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. Mogući problemi s ovom slikom: Slika je pretamna. Slika je mutna. @@ -281,7 +284,7 @@ Promijenio/la sam mišljenje, ne želim da više bude javno vidljivo Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće. Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo! - Molimo Vas, pričekajte ... + Molimo Vas, pričekajte … Preskoči ovu sliku Zadani jezik za opis Pokušavanje ažuriranja kategorija. @@ -293,7 +296,7 @@ Dodano u oznake Nešto je pošlo po zlu. Ne možemo postaviti pozadinu Postavi kao pozadinu - Postavljanje pozadine. Molimo, pričekajte... + Postavljanje pozadine. Molimo, pričekajte… Zadano Tamno Svijetlo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index aefc17d9d5..eb34386749 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -441,7 +441,7 @@ Sorozatszámok Szoftver Képek feltöltése Wikimedia Commons-ba közvetlenül a telefonodról. Töltsd le a Commons applikációt most: %1$s - Alkalmazás megosztása ezzel... + Alkalmazás megosztása ezzel… Képinformáció Nem található kategória Megszakított feltöltés @@ -474,7 +474,7 @@ Híd, múzeum, szálloda, stb. A belépés nem sikerült, kérj új jelszót. Beállítás háttérképnek - Beállítás háttérképnek. Kérem várjon... + Beállítás háttérképnek. Kérem várjon… Rendszerbeállítás követése Sötét Világos diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 219fa45210..8fff554e31 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,7 +61,6 @@ %1$d Unggahan - Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Jelajahi @@ -82,7 +81,7 @@ Memasuki log Silakan tunggu… Memperbarui takarir dan deskripsi - Mohon tunggu... + Mohon tunggu… Berhasil masuk log! Gagal masuk log! Berkas tidak ditemukan. Silakan coba berkas lain. @@ -191,7 +190,7 @@ Ya! Informasi selengkapnya Kategori - Memuat... + Memuat… Tidak ada yang dipilih Tanpa takarir Tidak ada keterangan @@ -497,7 +496,7 @@ Akses lokasi media ditolak Kami mungkin tidak dapat memperoleh data lokasi secara otomatis dari gambar yang Anda unggah. Harap tambahkan lokasi yang sesuai untuk setiap gambar sebelum mengirimkannya Mengunggah foto ke Wikimedia Commons secara langsung dari telepon Anda. Unduh aplikasi Commons sekarang: %1$s - Bagikan aplikasi lewat... + Bagikan aplikasi lewat… Info Gambar Kategori tidak ditemukan Penggambaran tidak ditemukan @@ -523,7 +522,6 @@ Pembaruan kategori Berhasil - Kategori %1$s ditambahkan. Kategori %1$s ditambahkan. Tidak bisa menambahkan kategori. @@ -569,7 +567,7 @@ Ditambahkan ke pembatas Terjadi kesalahan. Tidak bisa menetapkan wallpaper Jadikan Wallpaper - Sedang menetapkan Wallpaper. Tolong tunggu... + Sedang menetapkan Wallpaper. Tolong tunggu… Ikuti sistem Gelap Terang @@ -625,9 +623,9 @@ Mode Koneksi Terbatas Gambar Berkualitas Gambar berkualitas adalah diagram atau foto yang memenuhi standar kualitas tertentu (yang sifatnya teknis) dan berharga bagi proyek Wikimedia - Melanjutkan unggahan... - Menunda unggahan... - Membatalkan pengunggahan... + Melanjutkan unggahan… + Menunda unggahan… + Membatalkan pengunggahan… Batalkan pengunggahan Anda menyalakan mode koneksi terbatas. Semua pengunggahan ditunda dan akan dilanjutkan begitu Anda mematikan mode ini. Mode sambungan terbatas sedang menyala. @@ -743,7 +741,7 @@ %d gambar dipilih Bicara - Membatalkan semua unggahan... + Membatalkan semua unggahan… Unggahan Menunggu Gagal diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 51fe164419..994b1c3d34 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -70,9 +70,9 @@ Ka tu obliviis tua pasovorto? Enirar Eniranta - Voluntez vartar... + Voluntez vartar… Aktualiganta etiketi e deskripturi - Voluntez vartar... + Voluntez vartar… Eniro sucesoza! Eniro faliis! Arkivo ne trovita. Voluntez probar altr arkivo. @@ -142,7 +142,7 @@ Sendez komenti (per e-posto) Nula kliento di e-posto instalesis Kategorii recente uzita - Vartanta unesma sinkronigo... + Vartanta unesma sinkronigo… Vu ankore ne sendis fotografuri. Riprobar Nuligar @@ -180,7 +180,7 @@ Yes! Plusa informo Kategorii - Karganta... + Karganta… Nulo selektesis Nula deskripto-texto Nula deskripto @@ -410,7 +410,7 @@ Vu ne lektis irga avizo Vidar lektita Vidar ne-lektata - Vartez... + Vartez… Kopiita Exempli pri bona imaji por sendar a Commons Saltez ca imajo @@ -472,7 +472,7 @@ Ajusti Adjuntita marko-rubandi Uzar kom skreno-kovrilo - Kreanta skreno-kovrilo. Voluntez vartar... + Kreanta skreno-kovrilo. Voluntez vartar… Koloro obskura Koloro klara Charjez pluse @@ -500,7 +500,7 @@ Uzita Mea rango Imaji di qualeso - Nuliganta sendajo... + Nuliganta sendajo… Cesar kargajo Lektez pluse En omna idiomi diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 4176529534..ac64fbf2cd 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ * Sveinki * Sveinn í Felli --> - + Commons Facebook-síðan Grunnkóði Commons á Github Táknmerki Commons @@ -51,7 +51,7 @@ %1$d innsendingar - Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns + Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndaanna og gerð tækisins þíns Uppgötva @@ -138,7 +138,7 @@ Senda umsögn (með tölvupósti) Ekkert tölvupóstforrit er uppsett Nýlega notaðir flokkar - Bíð eftir fyrstu samstillingu... + Bíð eftir fyrstu samstillingu… Þú ert ekki ennþá búin(n) að senda inn neinar myndir. Reyna aftur Hætta við @@ -477,7 +477,7 @@ Hugbúnaður Aðgangi að staðsetningu gagnamiðla hafnað Sendu myndir inn á Wikimedia Commons beint úr símanum þínum. Sæktu Commons-appið núna: %1$s - Deila forriti með... + Deila forriti með… Upplýsingar í mynd Engir flokkar fundust Engar myndlýsingar fundust diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f408638708..e9aa8934ee 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -46,31 +46,38 @@ Foto del giorno %1$d file in caricamento + %1$d file in caricamento %1$d file in caricamento (%1$d) + (%1$d) (%1$d) Avvio del caricamento Elaborando %d caricamento + Elaborando %d caricamenti Elaborando %d caricamenti %d caricamento + %d caricamenti %d caricamenti Questa immagine sarà rilasciata in base alla licenza %1$s + Queste immagini saranno rilasciate in base alla licenza %1$s Queste immagini saranno rilasciate in base alla licenza %1$s %1$d caricamento + %1$d caricamenti %1$d caricamenti Ricezione di contenuti condivisi. L\'elaborazione dell\'immagine potrebbe richiedere del tempo a seconda delle dimensioni dell\'immagine e del dispositivo + Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Esplora @@ -516,7 +523,7 @@ Accesso alla posizione multimediale negato Potremmo non essere in grado di ottenere automaticamente i dati sulla posizione dalle immagini caricate. Si prega di aggiungere la posizione appropriata per ciascuna immagine prima di inviarla Carica foto su Wikimedia Commons direttamente dal tuo telefono. Scarica subito l\'app Commons: %1$s - Condividi applicazione tramite... + Condividi applicazione tramite… Informazioni sull\'immagine Nessuna categoria trovata Nessuna definizione trovata @@ -543,6 +550,7 @@ Successo Categoria %1$s aggiunta. + Categorie %1$s aggiunte. Categorie %1$s aggiunte. Non è stato possibile aggiungere le categorie. @@ -575,7 +583,7 @@ Esiste Necessita della fotografia Tipo di luogo: - Ponte, museo, albergo, ecc... + Ponte, museo, albergo, ecc… Si è verificato un errore durante l\'accesso. Devi reimpostare la password! MEDIA CLASSI FIGLIE @@ -588,7 +596,7 @@ Aggiungi ai preferiti Qualcosa è andato storto. Non è stato possibile impostare lo sfondo schermo Imposta come sfondo - Impostazione di sfondo in corso... + Impostazione di sfondo in corso… Segui sistema Scuro Chiaro @@ -758,6 +766,7 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata + %d immagini selezionate %d immagini selezionate Questo posto non ha ancora una foto, scattane una! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 0b512102b4..4b8c51f6c1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -45,44 +45,37 @@ מועלה קובץ אחד מועלים %1$d קבצים - מועלים %1$d קבצים מועלים %1$d קבצים (%1$d) (%1$d) - (%1$d) (%1$d) ההעלאות מתחילות עיבוד העלאה עיבוד d% העלאות - עיבוד d% העלאות עיבוד d% העלאות העלאה אחת %d העלאות - %d העלאות %d העלאות התמונה הזאת תפורסם ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s - התמונות האלה תפורסמנה ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s העלאה אחת %1$d העלאות - %1$d העלאות %1$d העלאות מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך - מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך לחקור @@ -101,9 +94,9 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין... + נא להמתין… עדכון כיתובים ותיאורים - נא להמתין... + נא להמתין… הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -213,7 +206,7 @@ כן! מידע נוסף קטגוריות - בטעינה... + בטעינה… לא נבחר דבר אין כיתוב אין תיאור @@ -508,7 +501,7 @@ הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין... + נא להמתין… תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -530,7 +523,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך... + שיתוף היישום דרך… פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -558,7 +551,6 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. - נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -568,7 +560,6 @@ נוסף מוצג %1$s נוספו המוצגים %1$s - נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -611,7 +602,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין... + הגדרת טפט. נא להמתין… מערכת מעקב כהה בהירה @@ -671,7 +662,7 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה... + ביטול ההעלאה… ביטול ההעלאה הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. @@ -801,7 +792,6 @@ נבחרה תמונה אחת נבחרו שתי תמונות - נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -815,7 +805,7 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות... + ביטול כל ההעלאות… העלאות ממתינות נכשלו diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f20b986f82..f60bb30ddc 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -44,7 +44,6 @@ %1$d 件のファイルをアップロード中 - (%1$d) (%1$d) アップロードを開始中です @@ -55,14 +54,12 @@ %d 件のアップロード - この画像は%1$sライセンスのもとにアップロードされます これらの画像は%1$sライセンスのもとにアップロードされます %1$d 件のアップロード - 共有コンテンツを受信中です。 この画像の投稿の処理には、サイズやご使用の機器により時間がかかる事があります 共有コンテンツの受信中です。投稿画像の処理には、サイズやご使用の機器により時間がかかる事があります 探索 @@ -560,7 +557,7 @@ ブックマークに追加 問題が発生しました。壁紙を設定できませんでした。 壁紙として設定 - 壁紙を設定中。お待ちください... + 壁紙を設定中。お待ちください… システムのまま ダーク ライト diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 40eb016295..eb90e4a23c 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -47,8 +47,8 @@ Qqen Tettuḍ awal uffir? Jerred - Tuqqna... - Rǧu... + Tuqqna… + Rǧu… Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -100,7 +100,7 @@ Azen tikti (s yimayl) Ulac amsaɣ n yimayl ibedden Taggayin yettwasqedcenmelmi kan - Araǧu n umtawi amezwaru... + Araǧu n umtawi amezwaru… Ur tsuliḍ ara yakan tiwlafin. Ɛref̣ tikelt-nniḍen Sefsex @@ -130,7 +130,7 @@ Tɣileḍ igarrez? Ih! Taggayin - Asali... + Asali… Ula d yiwet ur tettwafren Ulac aglam Turagt tarussint diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index b729838b97..3703d373fb 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,28 +43,22 @@ 검색 뷰 오늘의 이미지 - %1$d개의 파일을 올리는 중 %1$d개의 파일을 올리는 중 - (%1$d) (%1$d) 파일 올리기 - %1$d장의 업로드를 처리하는 중입니다 %1$d장의 업로드를 처리하는 중입니다 - %d개 업로드 %d개 업로드 - 이 그림은 %1$s에 따라 사용이 허가됩니다 이 그림은 %1$s에 따라 사용이 허가됩니다 - %1$d개 업로드 %1$d개 업로드 찾아보기 @@ -85,7 +79,7 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오... + 기다려 주십시오… 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -456,7 +450,7 @@ 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오... + 기다려 주십시오… 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -471,7 +465,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유... + 앱 공유… 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -529,7 +523,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오... + 배경화면을 설정 중입니다. 기다려 주십시오… 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index b976848217..be63e9db5b 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -143,7 +143,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады... + Биринчи синхронизацияны сакълаб турады… Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -498,7 +498,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле... + Къошакъны буну бла юлюшле… Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +575,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +633,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады... - Джюклениу туракъланады... - Джюклениу ызына алынады... + Джюклениу андан ары бардырылады… + Джюклениу туракъланады… + Джюклениу ызына алынады… Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 506e9e4b47..d9d5b65b91 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -70,8 +70,8 @@ Te şîfreya xwe ji bîr kir? Xwe tomar bike Têdikeve - Ji kerema xwe piçek bisekine ... - Xêra xwe hinek bisekine... + Ji kerema xwe piçek bisekine … + Xêra xwe hinek bisekine… Têketin bi ser ket! Têketin bi ser neket! Dosye nehat dîtin. Ji kerema xwe re dosyeyek din biceribîne. @@ -183,7 +183,7 @@ Wêneyên Barkirî Wêneyê din Belê, çima na - Ji kerema xwe piçek bisekine ... + Ji kerema xwe piçek bisekine … Wêne tevlî Wîkîpediyayê bike Tu dixwazî vê wêneyê tevlî gotara Wîkîpediyayê ya bi zimanê %1$s bikî? Pişrast bike diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index 8112afea61..ab657b354a 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -49,7 +49,7 @@ Юклев уьлгю: Дюр! Категориялар - Юклев... + Юклев… Бир зат сайланмагъан Тасвири ёкъ Пикирлешивлер ёкъ diff --git a/app/src/main/res/values-kus/strings.xml b/app/src/main/res/values-kus/strings.xml index 99fb8c1f74..02abd4ea10 100644 --- a/app/src/main/res/values-kus/strings.xml +++ b/app/src/main/res/values-kus/strings.xml @@ -62,9 +62,9 @@ Fʋ tami fʋ paaswɛɛtɛ? Yɔ\'ɔgin kpɛn\' Kpɛn\'ɛdnɛ - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Maligim maal pian\'azut nɛ pa\'alʋg nam - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Kpɛn\'ɛb nyaŋya Kpɛn\'ɛb gʋ\'ʋŋya M Pʋ nyɛ faal la. M bɛlimnɛ tiakim faal si\'a. @@ -169,7 +169,7 @@ Ɛɛn! Labaya bɛdigʋ Buudi kɔn\'ɔb-kɔn\'ɔb - Bɛ tʋʋma ni... + Bɛ tʋʋma ni… Pʋ gaŋ si\'ela Pian\'azug kae Pa\'alʋg kae @@ -400,7 +400,7 @@ Gɔsim dinɛ ka fʋ karim sa Gɔsim dinɛ ka fʋ nam pʋ karim Daʋŋʋ kidig footonam la nɔkirin - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Footo banɛ ka fʋ kpɛn\'ɛsi dɔlis zin\'ibanɛ be yamma anɛ footo banɛ ka fʋ kpɛn\'ɛs ka di yinɛ fʋn nyɛ di map ni la. Yaam paas media banɛ bɛ tuon Yaaiya @@ -418,7 +418,7 @@ Serial Numbers Software Pʋ bas suor ye fʋ kpɛn\' midia zin\'iginɛ - Pʋdigim app la dɔlis... + Pʋdigim app la dɔlis… Footo labaar Pʋ paam buudinama Pʋ nyɛ nwɛnnɛm si\'aa. @@ -492,7 +492,7 @@ Ba zaŋi paas bookmarknamin Daʋŋsi\'a naam. Pʋ nyaŋi maal nibdaa footo la Maalimi fʋ nindaa footo la - Maanɛ nindaa footo. M bɛlimnɛ gu\'usim... + Maanɛ nindaa footo. M bɛlimnɛ gu\'usim… Dɔl sistɛm la Lik Nɛɛsim @@ -538,9 +538,9 @@ Bas suor ye di tʋm saŋa bi\'ela! Atʋm bi\'ela zi\'esim Footo sʋma - Lɛm pin\'in kpɛn\'ɛsʋg... - Gu\'om kpɛn\'ɛsʋg... - Basid kpɛn\'ɛsʋg... + Lɛm pin\'in kpɛn\'ɛsʋg… + Gu\'om kpɛn\'ɛsʋg… + Basid kpɛn\'ɛsʋg… Basim kpɛn\'ɛsʋg Bas suor ye di tʋm saŋa bi\'ela. Nwɛnnɛm nam diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 2eb2fcf2f5..8b2ab6b955 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -21,7 +21,6 @@ %1$d файл жүктөлүүдө - Азырынча жүктөөлөр жок 1 жүктөө %1$d жүктөө @@ -137,7 +136,7 @@ Жүктөөнү жокко чыгаруу Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз Жүктөөнү улантуу - Күтө туруңуз... + Күтө туруңуз… Аталыш Сыпаттама Элементтер diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 9d69efabb7..d99e269ab1 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -61,7 +61,7 @@ Aloggen Waart wgl. … Beschrëftungen a Beschreiwungen aktualiséieren - Waart wgl. ... + Waart wgl. … Umeldung huet geklappt! D\'Aloggen huet net funktionéiert! Fichier net fonnt. Probéiert wgl. en anere Fichier. @@ -349,7 +349,7 @@ Déi geliese weisen Déi net geliese weisen Feeler beim Eraussiche vun de Biller - Waart wgl. ... + Waart wgl. … Kopéiert Beispiller vu gudde Biller fir op Commons eropzelueden Beispiller fir Biller, déi een net eropluede sollt @@ -361,7 +361,7 @@ Seriennummeren Software Luet Fotoen direkt vun Ärem Handy op Wikimedia Commons erop. Luet d\'Commons-App elo erof: %1$s - App deelen iwwer... + App deelen iwwer… Bildinformatiounen Keng Kategorie fonnt. Eroplueden ofgebrach @@ -411,7 +411,7 @@ Bei d\'Lieszeechen derbäigesat Et ass Eppes schif gaangen. D\'Hannergrondbild konnt net agestallt ginn Als Hannergrondbild festleeën - Hannergrondbild gëtt agestallt. Waart wgl... + Hannergrondbild gëtt agestallt. Waart wgl… System suivéieren Däischter Hell @@ -454,7 +454,7 @@ Limitéierte Verbindungsmodus Qualitéitsbiller Qualitéitsbiller sinn Diagrammen oder Fotoen, déi gewësse Qualitéitscritèren erfëllen (déi haaptsächlech vun technescher Natur sinn) a wäertvoll fir Wikimedia-Projete sinn. - Eropluede gëtt ofgebrach.... + Eropluede gëtt ofgebrach…. Eroplueden ofbriechen Kategoriesäit weisen Sprooch vum Interface vum Benotzer vun der App diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index f477ed8f0b..1720bfbcb4 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -33,8 +33,8 @@ Melj dich aan Wachwaord vergaete? Teiken dich in - Aan \'nt melje... - Wach estebleef... + Aan \'nt melje… + Wach estebleef… Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. @@ -88,7 +88,7 @@ Sjik feedback (mitten e-mail) Geine e-mailcliënt geïnstalleerd Recèntelik gebroekde categorieje - Oppe ieëste synchronisatie \'nt wachte... + Oppe ieëste synchronisatie \'nt wachte… Doe höbs nag gein plaetjes geüpload. Perbeer oppernuuj Braek aaf @@ -127,7 +127,7 @@ Versteis se \'t? Jao! Categorieje - \'nt laje... + \'nt laje… Geine gekaoze Gein besjrieving Ónbekande licentie diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 26a9bc7f77..cb7bebe41f 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -33,20 +33,27 @@ Dienos nuotrauka %1$d keliamas failas + %1$d keliami failai + %1$d failų keliamas %1$d keliami failai + %1$d įkėlimas + %1$d įkėlimai + %1$d įkėlimų - \@string/contributions_subtitle_zero - 1 įkėlimas Įkėlimai pradedami Pradedamas %1$d įkėlimas + Pradedami %1$d įkėlimai + Pradedami %1$d įkėlimų Pradedami %1$d įkėlimai %1$d įkėlimas + %1$d įkėlimai + %1$d įkėlimų %1$d įkėlimai Šio paveikslėlio licencija bus %1$s @@ -68,7 +75,7 @@ Jungiamasi Prašome palaukti… Antraštės ir aprašymai atnaujinami - Prašome palaukti... + Prašome palaukti… Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. @@ -172,7 +179,7 @@ Taip! Daugiau informacijos Kategorijos - Kraunasi... + Kraunasi… Niekas nepasirinkta Nėra antraštės Nėra aprašymo @@ -465,7 +472,7 @@ Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida - Prašome palaukti... + Prašome palaukti… Rinktinės nuotraukos yra aukštos kvalifikacijos fotografų ir iliustratorių vaizdai, kuriuos Vikiteka bendruomenė pasirinko kaip svetainėje aukščiausios kokybės. Vaizdai, įkelti per Netoliese esančias vietas, yra vaizdai, kurie įkeliami atrandant vietas žemėlapyje. Ši funkcija leidžia redaktoriams siųsti padėkos pranešimą naudotojams, kurie atlieka naudingus pakeitimus, naudojant nedidelę padėkos nuorodą istorijos puslapyje arba skirtumų puslapyje. @@ -486,7 +493,7 @@ Prieiga prie medijos vietos uždrausta Gali būti, kad negalėsime automatiškai gauti vietos duomenų iš jūsų įkeltų nuotraukų. Prieš pateikdami kiekvienai nuotraukai pridėkite tinkamą vietą Įkelkite nuotraukas į Vikiteką tiesiai iš savo telefono. Atsisiųskite Vikitekos programėlę dabar: %1$s - Dalintis programą per ... + Dalintis programą per … Vaizdo informacija Kategorijų nerasta Vaizdų nerasta @@ -746,7 +753,7 @@ Kita problema arba informacija (paaiškinkite toliau). Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> Ar tikrai norite atšaukti visus įkėlimus? - Atšaukiami visi įkėlimai... + Atšaukiami visi įkėlimai… Įkėlimai Laukiama Nepavyko diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 7a6d9e3628..9038eec9de 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,7 @@ Reģistrēties Pieslēdzas Lūdzu, uzgaidiet… - Lūdzu, uzgaidi... + Lūdzu, uzgaidi… Ieiešana veiksmīga Pieteikšanās neizdevās. Autentifikācija neizdevās! @@ -163,7 +163,7 @@ Nākamais attēls Skatīt arhivētos Skatīt nelasītos - Lūdzu, uzgaidiet... + Lūdzu, uzgaidiet… Izlaist šo attēlu Autors Autortiesības diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 916f4f4202..c496505ae9 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4,7 +4,7 @@ * Violetova * Vlad5250 --> - + Ризницата на Фејсбук Изворен код на Ризницата на Github Лого на Ризницата @@ -52,7 +52,7 @@ %1$d подигања - Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред + Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликите и вашиот уред Истражи @@ -73,7 +73,7 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте... + Почекајте… Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -479,7 +479,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте... + Почекајте… Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +501,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку... + Сподели преку… Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -780,7 +780,7 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања... + Ги откажувам сите подигања… Подигања Во исчекување Неуспешно diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index de888dcbc3..0d8e029a4c 100644 --- a/app/src/main/res/values-mni/strings.xml +++ b/app/src/main/res/values-mni/strings.xml @@ -18,7 +18,7 @@ ꯈꯨꯠꯌꯦꯛ ꯄꯤꯈꯠꯂꯨ ꯃꯅꯨꯡ ꯆꯪꯁꯤꯟꯂꯤ ꯉꯥꯏꯍꯥꯛ ꯉꯥꯏꯕꯤꯌꯨ - ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ... + ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ… ꯃꯥꯏꯄꯥꯛꯅꯥ ꯆꯪꯁꯤꯜꯂꯦ ꯫ ꯆꯪꯁꯤꯟꯕ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ! ꯐꯥꯏꯜ ꯊꯤꯕꯥ ꯐꯪꯗꯔꯦ ꯫ ꯆꯥꯟꯕꯤꯗꯨꯅꯥ ꯑꯇꯣꯞꯄ ꯑꯃꯥ ꯇꯧꯕꯤꯔꯣ ꯫ @@ -59,7 +59,7 @@ ꯍꯣꯏ! ꯑꯍꯦꯟꯕ ꯋꯥꯔꯣꯜ ꯃꯆꯥꯈꯥꯏꯕꯁꯤꯡ - ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ..... + ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ….. ꯑꯃꯠꯇ ꯈꯟꯗꯦ ꯑꯀꯨꯞꯄ ꯃꯔꯣꯜ ꯌꯥꯎꯗꯦ ꯈꯟꯅ-ꯅꯩꯅꯕ ꯂꯩꯇꯦ diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index a6c18bca30..27a76b0a75 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -45,7 +45,7 @@ ဝိုတ်စ မအက္ခရ်ပၞုက် ပတိုန် စၟတ်သမ္တီ လုပ်လံက်အေန် ဒၟံင် - ပဂုန်တုဲ မင်မွဲလစုတ်... + ပဂုန်တုဲ မင်မွဲလစုတ်… လုက်အေန် အာစိုပ်ဒတုဲ! လံက်အေန် လီုလာ်! ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။ @@ -148,7 +148,7 @@ ယွံ! ဆက်လဴ ပရူတင်ဂၞင် ကဏ္ဍဂမၠိုင် - ပတိုန်ဒၟံင်... + ပတိုန်ဒၟံင်… ဟွံမဲကဵု ပရေၚ်ရုဲစှ် ဟွံမဲကဵု က္ဍိုပ်လိက် ဟွံမဲကဵု ဗမံက်ထ္ၜး diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 546b43f4fa..9655945855 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -17,7 +17,6 @@ %1$d संचिका अपभारीत होत आहे - अद्याप अपभारणे नाहीत एक अपभारण %1$d अपभारणे @@ -94,7 +93,7 @@ प्रतिसाद पाठवा (विपत्राद्वारे) कोणतेही ईमेल क्लायंट स्थापित नाहीत अलीकडे वापरलेले वर्ग - प्रथम संकालनाची प्रतीक्षा करीत आहे ... + प्रथम संकालनाची प्रतीक्षा करीत आहे … आपण अद्याप काहीच चित्रे अपभारीत केली नाहीत. पुन्हा प्रयत्न करा रद्द करा diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 1fce0c0daf..e5dd0f3beb 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -19,20 +19,16 @@ အားလုံး ယနေ့အတွက် အထူးဓာတ်ပုံ - ဖိုင် %1$d ခု တင်နေသည် ဖိုင် %1$d ခု တင်နေသည် အပ်ပလုဒ်များ စတင်ခြင်း - %1$d ခု တင်ထားသည် %1$d ခု တင်ထားသည် - ဤရုပ်ပုံသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် ဤရုပ်ပုံများသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် - %1$d အက်ပလုပ် %1$d အက်ပလုပ်များ ရှာဖွေစူးစမ်းပါ @@ -49,9 +45,9 @@ အကောင့်ဝင်ရန် စကားဝှက် မေ့နေပါသလား မှတ်ပုံတင်ရန် - လော့ဂ်အင် ဝင်ရောက်နေသည်... - ခေတ္တစောင့်ပါ... - ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + လော့ဂ်အင် ဝင်ရောက်နေသည်… + ခေတ္တစောင့်ပါ… + ကျေးဇူးပြု၍ ခဏစောင့်ပါ… လော့အင် အောင်မြင်သည် လော့အင် မအောင်မြင်ပါ ဖိုင်မတွေ့ပါ၊ အခြးဖိုင်တစ်ခု စမ်းကြည့်ပါ။ @@ -133,7 +129,7 @@ ဟုတ်ကဲ့ သတင်းအချက်အလက် ပို၍ ကဏ္ဍများ - ဝန်ဆွဲတင်နေသည်... + ဝန်ဆွဲတင်နေသည်… ဘာမှရွေးချယ်မထားပါ ပုံစာ မရှိ ဖော်ပြချက် မရှိ @@ -315,7 +311,7 @@ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ရုပ်ပုံများကိုရွေးနေစဉ် အမှားဖြစ်ပွားခဲ့ပါသည် - ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + ကျေးဇူးပြု၍ ခဏစောင့်ပါ… နမူနာရုပ်ပုံများ အက်ပလုပ်တင်ရန် မဟုတ်ပါ ဤရုပ်ပုံအား ကျော်သွားမည် ဒေါင်းလုဒ် မအောင်မြင်ပါ။ ပြင်ပသိုလှောင်မှုခွင့်ပြုချက်မရှိဘဲ ဖိုင်ဒေါင်းလုဒ်မဆွဲနိုင်ပါ။ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index bf971f6bcc..3b4bf30dc9 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -522,7 +522,7 @@ Toegang tot medialocatie geweigerd Het is mogelijk dat we niet automatisch locatiegegevens kunnen verkrijgen van foto\'s die u uploadt. Voeg de locatie bij elke foto toe voordat u die upload Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s - App delen via... + App delen via… Afbeeldingsinfo Geen categorieën gevonden Geen beschrijvingen gevonden @@ -599,7 +599,7 @@ Als bladwijzer toegevoegd Er is iets fout gegaan. Kan de achtergrond niet instellen Instellen als achtergrond - Wordt ingesteld als achtergrond. Een ogenblik geduld... + Wordt ingesteld als achtergrond. Een ogenblik geduld… Systeem volgen Donker Licht @@ -659,7 +659,7 @@ Kwaliteitsafbeeldingen zijn diagrammen of foto\'s die voldoen aan bepaalde kwaliteitsnormen (die meestal technisch van aard zijn) en waardevol zijn voor Wikimedia-projecten Uploaden hervatten… Uploaden onderbreken… - Uploaden wordt geannuleerd... + Uploaden wordt geannuleerd… Uploaden Annuleren U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. Beperkte verbindingsmodus is ingeschakeld. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 7e11ea03a3..62e01d4d56 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -51,9 +51,9 @@ ߌ ߓߘߊ߫ ߢߌ߬ߣߊ߬ ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊ߫؟ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߝߍ߬ߛߓߍߟߌ ߣߌ߫ ߞߊ߲߬ߛߓߍߟߌ ߟߊߞߎߘߦߊ ߦߴߌ ߘߐ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫߹ ߞߐߕߐ߮ ߡߊ߫ ߛߐ߬ߘߐ߲߬. ߘߏ߫ ߜߘߍ߫ ߡߊߝߍߣߍ߲߫ ߖߊ߰ߣߌ߲߫. @@ -69,7 +69,7 @@ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲߬ ߞߐ߯ߟߕߊ ߟߎ߬ - ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫... + ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫… ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫ %1$d%% ߓߘߊ߫ ߘߝߊ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ @@ -148,7 +148,7 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -408,7 +408,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +418,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +486,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +533,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c097898e9f..eab67e0766 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -81,7 +81,7 @@ Mandar vòstres comentaris (per corrièl) Cap de client de corrièl pas installat Categorias utilizadas recentament - Espèra de primièra sincronizacion... + Espèra de primièra sincronizacion… Avètz pas encara telecargat cap de fòto. Tornar ensajar Anullar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8c64900a56..d0ee733960 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -8,7 +8,7 @@ * Sony dandiwal * ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ --> - + ਕਾਮਨਜ਼ ਮਾਰਕਾ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ @@ -17,11 +17,10 @@ ਸਾਰੇ ਦਿਨ ਦੀ ਤਸਵੀਰ - ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ + ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ %1$d ਫ਼ਾਈਲਾਂ ਚੜ੍ਹਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ - \@string/contributions_subtitle_zero %1$d upload %1$d ਅੱਪਲੋਡ @@ -30,7 +29,7 @@ %1$d ਸ਼ੁਰੂ ਹੋ ਰਹੇ ਹਨ - &d ਅੱਪਲੋਡ + %1$d ਅੱਪਲੋਡ %1$d ਅੱਪਲੋਡਾਂ ਇਹ ਤਸਵੀਰ ਦਾ %1$s ਹੇਠ ਲਸੰਸ ਜਾਰੀ ਕੀਤੀ ਜਾਵੇਗਾ @@ -45,7 +44,7 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -129,7 +128,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ... + ਲੱਦ ਰਿਹਾ ਹੈ… ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -201,7 +200,7 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ ਲਿਖਤ ਛਾਪੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index dcd8ea284a..09132f40e1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -508,7 +508,7 @@ Zobacz przeczytane Wyświetl nieprzeczytane Wystąpił błąd podczas pobierania zdjęć - Proszę czekać... + Proszę czekać… Polecane zdjęcia to zdjęcia wysoko wykwalifikowanych fotografów i ilustratorów, które społeczność Wikimedia Commons wybrała jako jedne z najwyższych jakości na stronie. Obrazy przesłane przez Pobliskie miejsca to obrazy, które są przesyłane przez odkrywanie miejsc na mapie. Ta funkcja umożliwia redaktorom wysyłanie powiadomień z podziękowaniem do użytkowników, którzy dokonują przydatnych zmian - za pomocą małego linku z podziękowaniem na stronie historii lub na stronie diff. diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 9c257c2735..b7449e9571 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -477,7 +477,7 @@ Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance - Ch\'a l\'abia passiensa... + Ch\'a l\'abia passiensa… Le fòto an evidensa a son ëd plance fàite da dij fotògraf e ilustrator motobin àbij che la comunità ëd Wikipedia Commons a l\'ha sernù tra cole ëd qualità pi àuta an sël sit. Le plance carià dai pòst ëd prossimità a son le plance carià con la dëscuverta dij pòst an sla carta. Costa fonsionalità a përmet ai contributor ëd mandé na notìfica d\'aringrassiament a j\'utent ch\'a fan dle modìfiche ùtij - an dovrand na cita liura d\'aringrassiament an sla pàgina dla stòria o cola dle diferense. @@ -499,7 +499,7 @@ Acess a la locassion dël mojen arfudà I podoma pa oten-e an automàtich ij dàit ëd localisassion dle plance che chiel a caria. Për piasì, ch\'a giontà la posission apropià për tute le plance prima ëd mandeje Ch\'a caria dle fòto su Wikimedia Commons diretaman da sò teléfon. Ch\'a dëscaria l\'aplicassion Commons adess: %1$s - Partagé l\'aplicassion via... + Partagé l\'aplicassion via… Anformassion an sla plancia Gnun-e categorìe trovà Gnun-e descrission trovà @@ -776,7 +776,7 @@ Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota). Ij sò sugeriment a saran giontà a coste pàgine wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> É-lo sigur ëd vorèj anulé tuti ij cariament? - Anulament ëd tuti ij cariament... + Anulament ëd tuti ij cariament… Cariament An atèisa Falì diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 4f17da26f7..461cb6b1d8 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -65,7 +65,7 @@ CC BY 3.0 هو وېشنيزې - رابرسېرېږي... + رابرسېرېږي… هېڅ هم نه دی ټاکل شوی څرگندونه نشته نامعلوم جواز diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3779a8a516..b0dd3b016a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,7 +28,7 @@ * Tuliouel * YuriNikolai --> - + Página do Commons no Facebook Código fonte do Commons no Github Logotipo do Commons @@ -51,32 +51,39 @@ Estado do local Imagem do Dia - carregando arquivo + carregando arquivo + carregando %1$d arquivos carregando %1$d arquivos (%1$d) + (%1$d) (%1$d) Iniciando carregamentos Processando %d carregamento + Processando %d carregamentos Processando %d carregamentos %d carregamento + %d carregamentos %d carregamentos Esta imagem será licenciada sob %1$s + Estas imagens serão licenciadas sob %1$s Estas imagens serão licenciadas sob %1$s %1$d carregamento + %1$d carregamentos %1$d carregamentos - Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo + Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo + Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Explorar @@ -95,9 +102,9 @@ Esqueceu a senha? Cadastre-se Efetuar login - Por favor, aguarde... + Por favor, aguarde… Atualizando legendas e descrições - Por favor, aguarde... + Por favor, aguarde… Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. @@ -161,7 +168,7 @@ Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"%1$s\">publicação no GitHub</a> para informar erros e sugestões. - Politica de privacidade + Política de privacidade Créditos Sobre Enviar comentários (por e-mail) @@ -248,7 +255,7 @@ Ponte de Arco-Íris Tulipa Bem-vindo à Wikipédia - Direitos de autor são bem vindo + Direitos de autor são bem-vindo Ópera de Sydney Cancelar Abrir @@ -306,7 +313,7 @@ Commons Avalie-nos Perguntas frequentes - Guia de usuario + Guia de usuário Pular Tutorial A Internet não está disponível Erro ao tentar obter as notificações @@ -521,7 +528,7 @@ Acesso à localização da mídia negado É possível que não possamos obter automaticamente os dados de localização das imagens que você carregar. Por favor adicione a localização adequada para cada imagem antes de envia-las Carregue fotos na wiki Wikimedia Commons, diretamente do seu celular. Baixe o aolicativo Commons agora: %1$s - Compartilhar aplicativo via... + Compartilhar aplicativo via… Informação da imagem Nenhuma categoria encontrada Nenhuma representação encontrada @@ -548,6 +555,7 @@ Sucesso A categoria %1$s foi adicionada. + As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -556,6 +564,7 @@ Editar representações O elemento retratado %1$s está adicionado. + Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -767,6 +776,7 @@ Salvar arquivo GPX %d imagem selecinada + %d imagens selecionadas %d imagens selecionadas Escreva algo sobre o item %1$s. Isso será visivel publicamente. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index bad9dc5005..19a52d72ff 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,7 @@ * Unamane * Vitorvicentevalente --> - + Página da wiki Commons no Facebook Código-fonte da wiki Commons no Github Logótipo da wiki Commons @@ -44,31 +44,38 @@ Imagem do Dia a carregar %1$d ficheiro + a carregar muitos %1$d ficheiros a carregar %1$d ficheiros (%1$d) + (%1$d) (%1$d) A iniciar carregamentos A processar %d carregamento + A processar %d carregamentos A processar %d carregamentos %d carregamento + %d carregamentos %d carregamentos Esta imagem será licenciada com a %1$s + Estas imagens serão licenciadas com a %1$s Estas imagens serão licenciadas com a %1$s %1$d carregamento + %1$d carregamentos %1$d carregamentos - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo A receber conteúdo partilhado. O processamento das imagens pode demorar algum tempo, dependendo do tamanho das mesmas e do seu dispositivo Explorar @@ -156,8 +163,8 @@ Política de privacidade Créditos Sobre - Enviar comentários (por correio eletrónico) - Não foi instalado nenhum cliente de correio eletrónico + Enviar comentários (por correio eletrónico) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. @@ -276,7 +283,7 @@ Gravar as fotografias tiradas com a câmara da aplicação no armazenamento do seu dispositivo Inicie sessão na sua conta Enviar ficheiro de registos - Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas + Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas Não foi encontrado nenhum navegador da Internet para abrir o URL Erro! Não foi possível encontrar o URL Nomear para eliminação @@ -491,7 +498,7 @@ Ver lidas Ver não lidas Ocorreu um erro ao escolher imagens - Aguarde, por favor... + Aguarde, por favor… As fotografias destacadas são imagens de fotógrafos e ilustradores altamente qualificados, que a comunidade da wiki Wikimedia Commons escolheu como as de melhor qualidade do \'\'site\'\'. As imagens carregadas via \"Locais próximos\" são as imagens que são carregadas descobrindo locais do mapa. Esta funcionalidade permite que os editores enviem uma notificação de agradecimento aos utilizadores que fizerem edições úteis - usando uma pequena hiperligação de agradecimento na página do historial ou na de diferenças. @@ -513,7 +520,7 @@ Acesso à localização de multimédia negado Podemos não conseguir obter automaticamente os dados de localização das fotografias que carregar. Adicione a localização apropriada de cada fotografia antes de a enviar, por favor Carregue fotografias na wiki Wikimedia Commons, diretamente do seu telemóvel. Descarregue a aplicação Commons agora: %1$s - Partilhar aplicação por... + Partilhar aplicação por… Informação da imagem Não foi encontrada nenhuma categoria Não foi encontrada nenhuma representação @@ -540,6 +547,7 @@ Êxito A categoria %1$s foi adicionada. + As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -548,6 +556,7 @@ Editar elementos retratados O elemento retratado %1$s está adicionado. + Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -589,7 +598,7 @@ Adicionado aos marcadores Ocorreu um problema. Não foi possível definir a imagem de fundo Definir como imagem de fundo - A definir a imagem de fundo. Aguarde, por favor... + A definir a imagem de fundo. Aguarde, por favor… Seguir sistema Escuro Claro @@ -645,8 +654,8 @@ Modo de ligação limitada Imagens de qualidade As imagens de qualidade são diagramas ou fotografias que satisfazem certos padrões de qualidade (principalmente de natureza técnica) e são valiosos para projetos da Wikimedia - A retomar carregamento... - A pausar carregamento... + A retomar carregamento… + A pausar carregamento… A cancelar o carregamento… Cancelar carregamento Ativou o modo de ligação limitada. Todos os carregamentos foram colocados em pausa e serão retomados quando desativar este modo. @@ -709,7 +718,7 @@ Não foi encontrada nenhuma localização Que tal adicionar o local onde a imagem foi tirada?\nOs dados de localização ajudam os editores da wiki a encontrarem a sua fotografia, tornando-a muito mais útil.\nObrigado! Adicionar localização - Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. + Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. Detalhes As realizações só estão disponíveis na versão de produção; consulte a documentação para programadores, por favor. A tabela de classificação só está disponível na versão prod. Consulte a documentação do desenvolvedor. @@ -760,6 +769,7 @@ Erro no envio de agradecimento ao autor. %d imagem selecionada + %d imagens selecionadas %d imagens selecionadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 0bcbc15506..ad1d0b805f 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,8 @@ %1$d de fișiere se încarcă - \@string/contributions_subtitle_zero (%1$d) + (%1$d) (%1$d) Pornirea încărcărilor @@ -74,8 +74,8 @@ V-ați uitat parola? Înregistrare Se conectează - Vă rugăm să așteptați ... - Vă rugăm să așteptați ... + Vă rugăm să așteptați … + Vă rugăm să așteptați … Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. @@ -458,7 +458,7 @@ Vezi citit Vezi necitit A apărut o eroare la alegerea imaginilor - Vă rugăm să așteptați ... + Vă rugăm să așteptați … Imaginile de Calitate sunt imagini ale unor fotografi și ilustratori de înaltă calificare, pe care comunitatea Wikimedia Commons a ales-o ca fiind de cea mai înaltă calitate pe site. Imaginile Încărcate prin Locurile din Apropiere sunt imaginile care sunt încărcate prin descoperirea locurilor de pe hartă. Această caracteristică permite editorilor să trimită o notificare de Mulțumire utilizatorilor care fac modificări utile - folosind un mic link de mulțumire pe pagina istoric sau pe pagina dif. @@ -478,7 +478,7 @@ Numere Serie Software Încărcați fotografii pe Wikimedia Commons direct de pe telefon. Descărcați aplicația Commons acum: %1$s - Partajează aplicația prin ... + Partajează aplicația prin … Informații despre imagine Nu s-au găsit categorii Nu s-au Găsit Reprezentări diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b15d777876..861d7ee27c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ * ЛингвоЧел * ОйЛ --> - + Facebook-страница Викисклада Исходный код Викисклада на гитхабе Логотип Викисклада @@ -105,7 +105,7 @@ %1$d загрузок - Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства + Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства @@ -556,7 +556,7 @@ Отказано в доступе к местоположению файла Возможно, мы не сможем автоматически получать данные о местоположении из загруженных вами изображений. Пожалуйста, добавьте подходящее место для каждого изображения перед отправкой Загружайте фото на Викисклад прямо с телефона. Скачайте приложение Wikimedia Commons прямо сейчас: %1$s - Поделиться приложением с помощью... + Поделиться приложением с помощью… Информация об изображении Категории не найдены. Описания не найдены @@ -637,7 +637,7 @@ Добавлено в закладки Что-то пошло не так. Не удалось установить фоновую заставку Сделать фоновой заставкой - Идёт установка фоновой заставки... + Идёт установка фоновой заставки… Настройки системы Тёмная Светлая @@ -695,8 +695,8 @@ Режим ограниченного подключения Качественные изображения Качественные изображения - это диаграммы или фотографии, которые соответствуют определенным стандартам качества (которые в основном носят технический характер) и представляют ценность для проектов Викимедиа - Возобновление загрузки... - Приостановка загрузки... + Возобновление загрузки… + Приостановка загрузки… Отмена загрузки… Отменить загрузку Вы включили ограниченный режим подключения. Все загрузки приостановлены и возобновятся после отключения этого режима. @@ -841,7 +841,7 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок... + Отмена всех загрузок… Загрузки В ожидании Не удалось diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index 08f9a1fecc..d4b5916593 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -169,7 +169,7 @@ ھا! وڌيڪ معلومات زمرا - لاهيندي... + لاهيندي… ڪوبہ چونڊيل ناھي عنوان ناهي ڪا تشريح ناھي @@ -315,7 +315,7 @@ لينس ماڊل سيريل انگ سافٽويئر - ايپ ذريعي ونڊيو... + ايپ ذريعي ونڊيو… عڪس معلومات زمرا نہ لڌا رد-ڪيل چاڙھ diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index 78114e3362..0489c363a1 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -44,9 +44,9 @@ Vajáldahttetgo beassansáni? Searvva Čáliha sisa - Vuordil... + Vuordil… Ođasmáhttá govvateavsttaid ja govvádusaid - Vuordil... + Vuordil… Sisačáliheapmi lihkostuvai! Sisačáliheapmi ii lihkostuvvan! Fiila ii gávdnon. Geahččal áinnas eará fiilla. @@ -112,7 +112,7 @@ Atte máhcahaga (e-poasttain) Ii leat ásahuvvon epoastadoaimmaheaddji Áitto geavahuvvon kategoriijat - Vuordime vuosttaš synkroniserema... + Vuordime vuosttaš synkroniserema… It leat vel bajásluđen ovttage gova. Geahččal ođđasit Gaskkalduhte @@ -143,7 +143,7 @@ Jua! Lassedieđut Kategoriijat - Luđeme... + Luđeme… Ii guhtege válljejuvvon Ii leat govvateaksta Ii gávdno govvádus diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 5997677efd..8e9cde75c9 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -112,7 +112,7 @@ Pošaljite Vašu povratnu informaciju (putem e-pošte) Nemate uspostavljen klijent za e-poštu Nedavno korištene kategorije - Čekam prvo usklađivanje... + Čekam prvo usklađivanje… Još uvijek niste otpremili nijednu sliku. Pokušaj ponovo Otkaži @@ -147,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje... + Učitavanje… Ništa nije odabrano Nema opisa Nema razgovora diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 92fa25f3eb..0e661acb74 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -5,25 +5,24 @@ * Sandaru * හරිත --> - + කොමන්ස් ෆේස්බුක් පිටුව කොමන්ස් ලාන්චනය කොමන්ස් වෙබ් අඩවිය - 1 ගොනුවක් උඩුගත කෙරේ + 1 ගොනුවක් උඩුගත කෙරේ ගොනු %d ක් උඩුගත කෙරේ - තවමත් කිසිදු උඩුගත කිරීමක් නැත - එක් උඩුගත කිරීමක් ඇත + එක් උඩුගත කිරීමක් ඇත උඩුගත කිරීම් %1$d ක් ඇත - 1 උඩුගත කිරීමක් ආරම්භ කරමින් + 1 උඩුගත කිරීමක් ආරම්භ කරමින් උඩුගත කිරීම් %1$d ක් ආරම්භ කරමින් - 1 උඩුගත කිරීමක් + 1 උඩුගත කිරීමක් උඩුගත කිරීම් %1$d ක් මෙම පින්තූරය %1$s යටතේ වලංගු වනු ඇත diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 49fc88a3b0..99a0bf5483 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -491,7 +491,7 @@ Zobraziť prečítané Zobraziť neprečítané Nastala chyba pri vyberaní obrázkov - Čakajte, prosím... + Čakajte, prosím… Najlepšie obrázky sú fotografie od vysoko skúsených fotografov a ilustrátorov, ktoré vybrala komunita Wikimedie Commons ako jedny z najkvalitnejších na stránke. Obrázky nahrané cez Miesta v okolí sú obrázky, ktoré sú nahrané vďaka objavovaniu miest na mape. Táto funkcia umožňuje poslať poďakovanie za užitočné úpravy používateľom – použitím malého odkazu poďakovať v histórií stránky alebo na stránke rozdielu medzi revíziami. @@ -513,7 +513,7 @@ Prístup k polohe médií bol odmietnutý Možno nebudeme môcť automaticky získať údaje o polohe z obrázkov, ktoré nahráte. Pred odoslaním, prosím, pridajte ku každému obrázku údaj o polohe. Nahrávajte fotky na Wikimedia Commons priamo z vášho mobilu. Stiahnite si aplikáciu Wikimedia Commons teraz: %1$s - Zdieľať aplikáciu cez... + Zdieľať aplikáciu cez… Informácie o obrázku Nenájdené žiadne kategórie Neboli nájdené spôsoby vykreslovania @@ -593,7 +593,7 @@ Pridané do záložiek Niečo sa pokazilo. Tapetu sa nepodarilo nastaviť Nastaviť ako tapetu - Nastavujem tapetu. Prosím, čakajte... + Nastavujem tapetu. Prosím, čakajte… Predvolený systém Tmavý Svetlý @@ -651,9 +651,9 @@ Mód limitovaného pripojenia Kvalitné obrázky Kvalitné obrázky sú diagramy a fotografie, ktoré spĺňajú určité štandardy (ktoré sú väčšinou technického charakteru) a sú cenné pre projekty Wikimédie - Pokračovanie nahrávania... - Pozastavovanie nahrávania... - Prerušovanie nahrávania... + Pokračovanie nahrávania… + Pozastavovanie nahrávania… + Prerušovanie nahrávania… Zrušiť nahrávanie Zapli ste mód limitovaného pripojenia. Všetky nahrávania budú teraz pozastavené a budú pokračovať až po vypnutí tohto módu. Mód limitovaného pripojenia je zapnutý. @@ -787,7 +787,7 @@ Iný problém alebo informácia (vysvetlite nižšie). Vaša spätná väzba sa zverejní na nasledujúcej wiki stránke: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Ste si istí, že chcete zrušiť všetky nahrávania? - Ruším všetky nahrávania... + Ruším všetky nahrávania… Nahrané súbory Čakajúce Zlyhané diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 61531980f1..b91c3c0b13 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ * McDutchie * Upwinxp --> - + Facebook stran Zbirke Izvorna koda Zbirke v shrambi Github Logotip Zbirke @@ -66,8 +66,8 @@ %1$d nalaganj - Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. - Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. @@ -87,9 +87,9 @@ Ste pozabili geslo? Ustvari račun Prijavljanje - Prosimo, počakajte ... + Prosimo, počakajte … Posodabljam napise in opise - Prosimo, počakajte ... + Prosimo, počakajte … Uspešno ste se prijavili! Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. @@ -133,7 +133,7 @@ Spremembe Naloži Poišči kategorije - Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) + Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, …) Shrani Osveži Seznam @@ -159,7 +159,7 @@ Pošljite povratno informacijo (prek e-pošte) Nameščen ni noben e-poštni odjemalec Pred kratkim uporabljene kategorije - Čakam na prvo sinhronizacijo ... + Čakam na prvo sinhronizacijo … Naložili niste še nobene fotografije. Poskusi znova Prekliči @@ -199,7 +199,7 @@ Da! Več informacij Kategorije - Nalaganje ... + Nalaganje … Nič ni izbrano Ni napisa Ni opisa @@ -491,7 +491,7 @@ Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake - Prosimo, počakajte ... + Prosimo, počakajte … Izbrane slike so slike izvrstnih fotografov in ilustratorjev, ki jih je skupnost Wikimedijine zbirke prepoznala kot najbolj kakovostne v tem projektu. Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. @@ -513,7 +513,7 @@ Dostop do lokacije predstavnosti zavrnjen Za slike, ki jih nalagate, ne moremo samodejno pridobiti lokacije. Pred pošiljanjem dodajte za vsako sliko ustrezno lokacijo. Nalagajte slike v Wikimedijino zbirko neposredno iz telefona. Prenesite aplikacijo Commons zdaj: %1$s - Deli aplikacijo prek ... + Deli aplikacijo prek … Informacije o sliki Ni najdenih kategorij Ni najdenih upodobitev @@ -569,7 +569,7 @@ Koordinat ni bilo mogoče pridobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise - Deli slike prek ... + Deli slike prek … Ničesar še niste prispevali %s ni opravil_a še nobenega prispevka Račun ustvarjen! @@ -593,7 +593,7 @@ Dodano med zaznamke Nekaj je šlo narobe. Ozadja ni bilo mogoče nastaviti. Nastavi kot ozadje - Nastavljam ozadje. Prosimo, počakajte ... + Nastavljam ozadje. Prosimo, počakajte … Sledi sistemu Temna Svetla @@ -649,9 +649,9 @@ Način omejene povezanosti Kakovostne slike Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie - Nalaganje se nadaljuje ... - Zaustavljam nalaganje ... - Preklicujem nalaganje ... + Nalaganje se nadaljuje… + Zaustavljam nalaganje… + Preklicujem nalaganje… Preklic nalaganja Vklopili ste način omejene povezanosti. Vsa nalaganja so začasno ustavljena in se bodo nadaljevala, ko boste ta način izklopili. Način omejene povezanosti je vklopljen. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index fe70bc6b6f..f1e7412d48 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -34,32 +34,39 @@ Слика дана %1$d датотека се отпрема + %1$d датотеке се отпремају %1$d датотеке се отпремају %1$d отпремање + %1$d отпремања %1$d отпремања Покретање отпремања Процесуирање %d отпремање + Процесуирање %d отпремања Процесуирање %d отпремања %d отпремање + %1$d отпремања %d отпремања Слика ће се водити под лиценцом %1$s + Слике ће се водити под лиценцом %1$s Слике ће се водити под лиценцом %1$s %1$d отпремање + %1$d отпремања %1$d отпремања - Примање дељеног садржаја... Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја - Примање дељеног садржаја... Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја Истрага Изглед @@ -496,7 +503,7 @@ Приступ локацији медија је одбијен Можда нећемо моћи да аутоматски прибавимо податке о локацији из слика које отпремите. Додајте одговарајућу локацију за сваку слику пре објављивања Отпреми фотографије на Викимедијину Оставу директно са свог телефона. Преузми апликацију Оставе сада: %1$s - Подели апликацију преко... + Подели апликацију преко… Информације о слици Нису пронађене категорије Отказано отпремање @@ -521,12 +528,13 @@ Успешно Категорија %1$s је додата. + Категорије %1$s су додате. Категорије %1$s су додате. Није могуће додати категорије. Ажурирај категорију Уреди приказе - Ажурирање координата... + Ажурирање координата… Ажурирање координата Ажурирање описа Ажурирање натписа @@ -729,6 +737,7 @@ Чување GPX датотеке %d слика је одабрана + %d слике су одабране %d слика је одабрано Унесите коментар diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 79ae5ea28f..64379ac928 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -26,32 +26,25 @@ Togel ka Luhur Gambar poé ieu - ngunjal %1$d berkas ngunjal %1$d berkas - (%1$d) (%1$d) Mitembeyan Ngamuat - Ngolah %d muatan Ngolah %d muatan - %1$d muatan %1$d muatan - Ieu gambar bakal dilisénsi %1$s Ieu gambar bakal dilisénsi %1$s - %1$d Dimuat %1$d Dimuat - Nampa kontén anu dibagikeun. Ngolah gambarna bisa jadi rada lila gumantung kana ukuran gambar jeung gaway anjeun Nampa kontén anu dibagikeun Langlang @@ -71,7 +64,7 @@ Asup log Tungguan… Nganyarkeun pertélaan jeung pedaran - Mangga tungguan... + Mangga tungguan… Laksana login! Gagal login! Berkas teu kapanggih. Coba berkas séjén. @@ -399,7 +392,7 @@ Tempo arsip Tempo nu can dibaca Éror pas keur nyomot gambar - Mangga tungguan... + Mangga tungguan… Iwalkeun ieu gambar Karya Hak cipta @@ -409,7 +402,7 @@ Nomer Seri Sopwér Muat poto ka Wikimedia Commons langsung tina ponsél. Unduh Commons App ayeuna: %1$s - Bagikeun app liwat... + Bagikeun app liwat… Info Gambar Euweuh Kategori kapanggih Muatan bedo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 370bf0915f..c49d64ad22 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -506,7 +506,7 @@ Åtkomst till mediaplats nekades Vi kanske inte automatiskt kan få platsdata från bilder du laddar upp. Lägg till lämplig plats för varje bild innan du skickar in Ladda upp foton till Wikimedia Commons direkt från din telefon. Ladda ned Commons-appen nu: %1$s - Dela appen via... + Dela appen via… Bildinfo Inga kategorier hittades Inga beskrivningar hittades @@ -783,7 +783,7 @@ Andra problem eller information (ange nedan). Din återkoppling kommer att skickas till följande wikisida: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a> Är du säker på att du vill avbryta alla uppladdningar? - Avbryter alla uppladdningar... + Avbryter alla uppladdningar… Uppladdningar Pågår Misslyckades diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index f9162bc7b4..4f41da5f6d 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -98,7 +98,7 @@ பின்னூட்டம் அனுப்பு (மின்னஞ்சல் வழியாக) மின்னஞ்சற் செயலி எதுவும் நிறுவப்படவில்லை அண்மையிற் பயன்படுத்தப்பட்ட பகுப்புகள் - முதல் ஒத்திசைவுக்காக காத்திருக்கிறது ... + முதல் ஒத்திசைவுக்காக காத்திருக்கிறது … நீர் இன்னும் எவ்வொளிப்படத்தையும் பதிவேற்றவில்லை. மீண்டும் முயல்க கைவிடு @@ -131,7 +131,7 @@ ஆம்! மேலதிக தகவல்கள் பகுப்புகள் - ஏற்றப்படுகிறது... + ஏற்றப்படுகிறது… தெரிவு செய்யப்படவில்லை தலைப்பு இல்லை விளக்கம் இல்லை diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index add46f7b7b..13ee985b95 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -110,7 +110,7 @@ ಇರೆನ ಅಬಿಪ್ರಾಯೊ ಬರೆಲೆ(ಮಿಂಚಂಚೆ). ಇರೆನ ಮಿಂಚಂಚೆ ಇಜ್ಜಿ. ಇಂಚಿಗ್ ಸೃಷ್ಟಿ ಮಾಲ್ತಿನ ವರ್ಗೊ. - ಒಂತೆ ಸಮಯ ಕಾಯೊಡು.... + ಒಂತೆ ಸಮಯ ಕಾಯೊಡು…. ಇರ್ ಒಂಜಿಲಾ ಪಟೋನ್ ಅಪ್ಲೋಡ್ ಮಾಲ್ತಿಜ್ಜಿ. ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ @@ -336,7 +336,7 @@ ಅನುರಕ್ಷಿತ ತೂಲೆ ಓದಂದಿನ ತೂಲೆ ಆಕೃತಿಲೆನ್ ಪೆಜ್ಜಿನಗ ದೋಷ ಆಂಡ್ - ದಯಮಲ್ತ್ ಕಾಪುಲೆ... + ದಯಮಲ್ತ್ ಕಾಪುಲೆ… ಸಂಯೋಜನೆಲು ಸೂಚನೆಲು ನನಾತ್ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index ae80a53357..0c478b2214 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -127,7 +127,7 @@ ఫీడుబ్యాకును పంపండి (ఈమెయిలు ద్వారా) ఈమెయిలు క్లయంటేదీ లేదు ఇటీవల వాడిన వర్గాలు - మొట్టమొదటి సింక్ కోసం చూస్తున్నాం... + మొట్టమొదటి సింక్ కోసం చూస్తున్నాం… ఇంకా మీరు ఫోటోలేమీ ఎక్కించలేదు. మళ్ళీ ప్రయత్నించు రద్దుచేయి @@ -457,7 +457,7 @@ క్రమ సంఖ్యలు సాఫ్టువేరు నేరుగా మీ ఫోను నుంచే వికీమీడియా కామన్స్‌కు ఫోటోలను ఎక్కించండి. కామన్స్ యాప్‌ను ఇప్పుడే దించుకోండి: %1$s - యాప్‌ను దీని ద్వారా పంచుకోండి... + యాప్‌ను దీని ద్వారా పంచుకోండి… బొమ్మ సమాచారం వర్గాలేమీ కనబడలేదు ఎక్కింపును రద్దు చేసాం @@ -523,7 +523,7 @@ బుక్‌మార్కులకు చేర్చాం ఏదో లోపం జరిగింది. వాల్‌పేపరును సెట్ చెయ్యలేకపోయాం వాల్‌పేపరుగా అమర్చు - వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి... + వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి… నల్లటి వెలుగుతో స్థానపు సెట్టింగులను తెరవడం విఫలమైంది. స్థానాన్ని మానవికంగా ఆన్ చెయ్యండి @@ -576,9 +576,9 @@ పరిమిత కనెక్షను మోడ్‌ను అచేతనం చేసాం. పెండింగులో ఉన్న ఎక్కింపులు తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ నాణ్యమైన బొమ్మలు - ఎక్కింపును తిరిగి మొదలెడుతున్నాం... - ఎక్కింపును నిలుపుతున్నాం... - ఎక్కింపును రద్దు చేస్తున్నాం... + ఎక్కింపును తిరిగి మొదలెడుతున్నాం… + ఎక్కింపును నిలుపుతున్నాం… + ఎక్కింపును రద్దు చేస్తున్నాం… ఎక్కింపును రద్దుచెయ్యి మీరు పరిమిత కనెక్షను మోడ్‌ను చేతనం చేసారు. ఎక్కింపులన్నీ నిలిచిపోయాయి. మీరు ఈ మోడ్‌ను అచేతనం చెయ్యగానే అవి తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ ఆన్ అయింది. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 125bba590f..70bee59ef5 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -38,21 +38,16 @@ รูปภาพประจำวัน กำลังอัปโหลดไฟล์ %1$d ไฟล์ - \@string/contributions_subtitle_zero - (%1$d) (%1$d) กำลังเริ่มอัปโหลด - กำลังเริ่มอัปโหลด %1$d รายการ กำลังเริ่มอัปโหลด %1$d รายการ - การอัปโหลด %1$d รายการ การอัปโหลด %1$d รายการ - ภาพนี้จะอยู่ในสัญญาอนุญาต %1$s ภาะเหล่านี้จะอยู่อยู่ในสัญญาอนุญาติ %1$s สำรวจ @@ -398,7 +393,7 @@ รุ่นเลนส์ หมายเลขซีเรียล ซอฟต์แวร์ - แบ่งปันแอปผ่าน... + แบ่งปันแอปผ่าน… ไม่พบหมวดหมู่ ภาพเซลฟี ภาพเบลอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 31a9f0b530..79482fb4e9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -210,7 +210,7 @@ Evet! Daha Fazla Bilgi Kategoriler - Yükleniyor... + Yükleniyor… Hiçbir şey seçilmedi Altyazı yok Açıklama yok @@ -505,7 +505,7 @@ Okunanları görüntüle Okunmayanları görüntüle Resimler seçilirken hata oluştu - Lütfen bekleyin... + Lütfen bekleyin… Seçkin resimler, Wikimedia Commons topluluğunun sitedeki en yüksek kaliteden bazıları olarak seçtiği son derece yetenekli fotoğrafçıların ve illüstratörlerin görüntüleridir. Yakındaki yerler üzerinden yüklenen resimler, haritadaki yerleri keşfederek yüklenen resimlerdir. Bu özellik, editörlerin, geçmiş sayfasında veya fark sayfasında küçük bir teşekkür bağlantısı kullanarak faydalı düzenlemeler yapan kullanıcılara bir Teşekkür bildirimi göndermesine olanak tanır. @@ -604,7 +604,7 @@ Yer işaretlerine eklendi Bir şeyler yanlış gitti. Duvar kağıdı ayarlanamadı Duvar kağıdı olarak ayarla - Duvar Kağıdı ayarlanıyor. Lütfen bekleyin... + Duvar Kağıdı ayarlanıyor. Lütfen bekleyin… Sistemi izle Koyu Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9e821ae24d..cc2343a771 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ * Ата * Пан Хаунд --> - + Facebook-сторінка Вікісховища Програмний код Вікісховища на GitHub Логотип Вікісховища @@ -81,7 +81,7 @@ %1$d завантажень - Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою + Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою @@ -612,7 +612,7 @@ Додано у закладки Щось трапилось. Не вдалося встановити шпалери робочого столу Встановити в якості шпалер робочого столу - Встановлення робочого столу. Будь ласка зачекайте... + Встановлення робочого столу. Будь ласка зачекайте… На взірець системи Темна Світла diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index a708c873a2..f09f76ac62 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -73,9 +73,9 @@ Parolni unutdingizmi? Roʻyxatdan oʻtish Kirish - Iltimos kuting... + Iltimos kuting… Sarlavhalar va tavsiflarni yangilash - Iltimos, kutib turing... + Iltimos, kutib turing… Kirish muvaffaqiyatli bajarildi! Kirish muvaffaqiyatsiz yakunlandi! Fayl topilmadi. Iltimos, boshqa faylni izlab koʻring. @@ -180,7 +180,7 @@ Ha! Batafsil maʼlumot Turkumlar - Yuklanmoqda... + Yuklanmoqda… Tanlanmagan Izoh yoʻq Tavsif yoʻq @@ -390,7 +390,7 @@ Xatchoʻplar Xatchoʻplar Bajarildi - Iltimos, kuting... + Iltimos, kuting… EXIF teglarni boshqarish Muallif Mualliflik huquqlari diff --git a/app/src/main/res/values-vec/strings.xml b/app/src/main/res/values-vec/strings.xml index bbcb64561a..52bb495ea8 100644 --- a/app/src/main/res/values-vec/strings.xml +++ b/app/src/main/res/values-vec/strings.xml @@ -68,7 +68,7 @@ Cargamento de %1$s no riusio Schicia par vixuałixare I me ultimi cargamenti - In coa... + In coa… Fałimento %1$d%% conpleto Drio cargar.. @@ -114,7 +114,7 @@ Mandane on comento (co ła mail) Nisun client de posta eletronega instałà Categorie doparà ultimamente - Speta par ła prima sincronixasion... + Speta par ła prima sincronixasion… No te ghe njiancora cargà na foto Riproa Descançełare @@ -403,7 +403,7 @@ Varda no lexeste Varda no lexeste Se ga vuo on eror co se jera drio ełexare łe imajini. - Speta on fià... + Speta on fià… Le foto in primo pian łe xé imajini de fotografi altamente cuałifegai che ła comunità de Wikimedia Commons ła ga ełeto come fotografi de alta cuałità sol sito. Imajini cargae via \"Posti cuà rente\", imajini che łe njien cargae scoerxendo posti n\'te ła mapa Sta funsion ła consente ai editori de enviar na notifega de ringrasiamento ai uxuari che i fa modifeghe che serve, doparando on lingambo picenin de ringrasiamento n\'te ła pajina del storego o n\'te ła pajina de łe difarense.\n\nQuesta funzione consente agli editor di inviare una notifica di ringraziamento agli utenti che apportano modifiche utili, utilizzando un piccolo link di ringraziamento nella pagina della cronologia o nella pagina delle differenze. @@ -421,7 +421,7 @@ Numari seriałi Software Carga foto so Wikimedia Commons diretamente dal to tełefonin. Descarga l\'aplicasion deso: %1$s - Spartisi aplicasion co... + Spartisi aplicasion co… Informasion so l\'imajine Nisuna categoria catada Cargamento nułà @@ -461,7 +461,7 @@ Xonta ai favorii Calcosa el xé ndà roerso. No xé sta pusibiłe canbiar el sfondo Inposta el sfondo - Drio inpostar el sfondo. Speta on fià... + Drio inpostar el sfondo. Speta on fià… Segui el sistema Scuro Ciaro diff --git a/app/src/main/res/values-xal/strings.xml b/app/src/main/res/values-xal/strings.xml index 346ff15e17..c362060613 100644 --- a/app/src/main/res/values-xal/strings.xml +++ b/app/src/main/res/values-xal/strings.xml @@ -23,15 +23,15 @@ Вики-аһулх һазр Тохрллһ Вики-аһулх һазрур ацалх - Ацалгдҗана... + Ацалгдҗана… Кергләчин нерн Нууц үг Невтрх Нууц үгән мартвт? Бүрткүлх Невтрҗәнә - Күләхнтн... - Күләхнтн... + Күләхнтн… + Күләхнтн… Невтрлт амҗлтта болла! Невтрҗ чадсн уга! Ацаллт кеҗ экллә! @@ -83,7 +83,7 @@ Тиим Делгрңгү Нерн, төрл - Умшҗана... + Умшҗана… Алькинь чигн суңһад уга Тодрхаллт уга Күүндән уга diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index b32eeb0057..7927da1af5 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -61,7 +61,7 @@ ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა - ეთმიხარგუ... + ეთმიხარგუ… მახვარებუშ ჯოხო პაროლი გენშართით თქვანი პროფილით Commons Beta-შა @@ -71,7 +71,7 @@ სისტემაშა მიშულა ქორთხინთ ქჷმიცადით … მუკნაჭარეფი დო ეჭარუეფი მითმიახალებუ - ქორთხინთ ქჷმიცადით... + ქორთხინთ ქჷმიცადით… სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. @@ -181,7 +181,7 @@ ქოǃ უმოსი ინფორმაცია კატეგორიეფი - იხარგუ... + იხარგუ… მუთუნ ვა რე გიშაგორილი მუკნაჭარა ვა რე ვა რე ეჭარუა diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index 27080b9991..c6f27bb991 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -13,7 +13,7 @@ ⵜⴻⵜⵜⵓⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵣⵔⴰⵢ? ⵣⵎⵎⴻⵎ ⴷⴰ ⵜⴽⵛⵛⵎⴷ - ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ... + ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ… ⴰⴽⵛⴰⵎ !ⵉⵎⵓⵔⵙ ⴰⴽⵛⴰⵎ ⵉⵣⴳⵍ! ⴰⴼⴰⵢⵍⵓ ⵓⵔ ⵉⵜⵜⵢⵓⴼⴰ. ⴰⵎⵓⵔ ⵏⵏⴽ ⴰⵔⵎ ⴰⴼⴰⵢⵍⵓ ⵢⴰⴹⵏ. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 74ff641c0b..2a307e955c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,28 +79,22 @@ 地点状态 今日图片 - %1$d个文件正在上传 %1$d个文件正在上传 - %1$d次上传 %1$d次上传 开始上传 - 正在处理%d个上传 正在处理%d个上传 - %d个上传 %d个上传 - 该图像的授权协议是 %1$s 这些图像的授权协议是 %1$s - %1$d次上传 %1$d次上传 @@ -552,7 +546,7 @@ 已拒绝访问媒体位置 我们可能无法自动从你上传的图片中获取位置数据。提交前请为每张图片添加适当的位置 直接在您手机上的维基共享资源应用中上传照片。立即下载共享资源应用:%1$s - 分享到... + 分享到… 图像信息 找不到分类 找不到描写。 @@ -578,7 +572,6 @@ 分类更新 成功 - 分类%1$s已添加。 分类%1$s已添加。 无法添加分类。 @@ -586,7 +579,6 @@ 正在尝试更新描述。 编辑描述 - 已添加 %1$s 个描写。 已添加 %1$s 个描写。 无法添加描述。 @@ -687,8 +679,8 @@ 限制连接模式 优良图片 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值 - 正在恢复上传... - 暂停上传... + 正在恢复上传… + 暂停上传… 正在取消上传… 取消上传 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。 @@ -816,7 +808,6 @@ 正在保存KML文件 正在保存GPX文件 - 已选择%d个图像 已选择%d个图像 请记住,每次多图片上传会为其中的所有图片标注相同的分类和描述。如果这些图片并不共享同样的描述和分类,请分别进行多次上传。 @@ -830,7 +821,7 @@ 其他问题或信息(请在下方解释)。 您的反馈已经发布在以下wiki页面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> 您确定要取消所有上传吗? - 取消所有的上传... + 取消所有的上传… 上传 待处理 失败 From 6adedd978935510bd7ba2557603551e04d8dcf8b Mon Sep 17 00:00:00 2001 From: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:02:35 +0530 Subject: [PATCH 003/231] fix test (#5893) Signed-off-by: parneet-guraya --- .../test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt index 283bbf268e..f980152dc8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/review/ReviewHelperTest.kt @@ -140,7 +140,7 @@ class ReviewHelperTest { mock().apply { whenever(title()).thenReturn(file) if (revision.isNotEmpty()) { - whenever(revisions()).thenReturn(*revision.toMutableList()) + whenever(revisions()).thenReturn(revision.toMutableList()) } val media = From d21c63f1dbb2874e8de1ecf21166d5b154e5f2e4 Mon Sep 17 00:00:00 2001 From: Parneet Singh <111801812+parneet-guraya@users.noreply.github.com> Date: Sat, 26 Oct 2024 19:49:34 +0530 Subject: [PATCH 004/231] `CommonsApplication` migrate to kotlin & some lint fixes (#5879) * convert to kotlin Signed-off-by: parneet-guraya * use lateinit instead of nullable types Signed-off-by: parneet-guraya * instance property access fix Signed-off-by: parneet-guraya * refactor constants name with uppercased ones Signed-off-by: parneet-guraya * remove unused Signed-off-by: parneet-guraya * fix imports in test Signed-off-by: parneet-guraya * use mockk for kotlin to fix tests Signed-off-by: parneet-guraya --------- Signed-off-by: parneet-guraya --- .../fr/free/nrw/commons/AboutActivityTest.kt | 2 +- .../free/nrw/commons/CommonsApplication.java | 432 ------------------ .../fr/free/nrw/commons/CommonsApplication.kt | 414 +++++++++++++++++ .../free/nrw/commons/actions/ThanksClient.kt | 2 +- .../free/nrw/commons/auth/LoginActivity.java | 8 +- .../description/DescriptionEditActivity.kt | 4 +- .../nrw/commons/upload/worker/UploadWorker.kt | 2 +- .../nrw/commons/actions/ThanksClientTest.kt | 7 +- .../DescriptionEditActivityUnitTest.kt | 8 + .../nrw/commons/upload/UploadClientTest.kt | 2 +- 10 files changed, 436 insertions(+), 445 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/CommonsApplication.java create mode 100644 app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt index b5a752ef90..45ff9e49dd 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt @@ -105,7 +105,7 @@ class AboutActivityTest { fun testLaunchTranslate() { Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) - val langCode = CommonsApplication.getInstance().languageLookUpTable.codes[0] + val langCode = CommonsApplication.instance.languageLookUpTable!!.codes[0] Intents.intended( CoreMatchers.allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java deleted file mode 100644 index 3aceb957ab..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ /dev/null @@ -1,432 +0,0 @@ -package fr.free.nrw.commons; - -import static fr.free.nrw.commons.data.DBOpenHelper.CONTRIBUTIONS_TABLE; -import static org.acra.ReportField.ANDROID_VERSION; -import static org.acra.ReportField.APP_VERSION_CODE; -import static org.acra.ReportField.APP_VERSION_NAME; -import static org.acra.ReportField.PHONE_MODEL; -import static org.acra.ReportField.STACK_TRACE; -import static org.acra.ReportField.USER_COMMENT; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.content.Context; -import android.content.Intent; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; -import android.os.Build; -import android.os.Process; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.multidex.MultiDexApplication; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.core.ImagePipelineConfig; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao; -import fr.free.nrw.commons.category.CategoryDao; -import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler; -import fr.free.nrw.commons.concurrency.ThreadPoolService; -import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.language.AppLanguageLookUpTable; -import fr.free.nrw.commons.logging.FileLoggingTree; -import fr.free.nrw.commons.logging.LogUtils; -import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; -import io.reactivex.Completable; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.internal.functions.Functions; -import io.reactivex.plugins.RxJavaPlugins; -import io.reactivex.schedulers.Schedulers; -import java.io.File; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import javax.inject.Inject; -import javax.inject.Named; -import org.acra.ACRA; -import org.acra.annotation.AcraCore; -import org.acra.annotation.AcraDialog; -import org.acra.annotation.AcraMailSender; -import org.acra.data.StringFormat; -import timber.log.Timber; - -@AcraCore( - buildConfigClass = BuildConfig.class, - resReportSendSuccessToast = R.string.crash_dialog_ok_toast, - reportFormat = StringFormat.KEY_VALUE_LIST, - reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL, - STACK_TRACE} -) - -@AcraMailSender( - mailTo = "commons-app-android-private@googlegroups.com", - reportAsFile = false -) - -@AcraDialog( - resTheme = R.style.Theme_AppCompat_Dialog, - resText = R.string.crash_dialog_text, - resTitle = R.string.crash_dialog_title, - resCommentPrompt = R.string.crash_dialog_comment_prompt -) - -public class CommonsApplication extends MultiDexApplication { - - public static final String loginMessageIntentKey = "loginMessage"; - public static final String loginUsernameIntentKey = "loginUsername"; - - public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled"; - @Inject - SessionManager sessionManager; - @Inject - DBOpenHelper dbOpenHelper; - - @Inject - @Named("default_preferences") - JsonKvStore defaultPrefs; - - @Inject - CommonsCookieJar cookieJar; - - @Inject - CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher; - - /** - * Constants begin - */ - - public static final String DEFAULT_EDIT_SUMMARY = "Uploaded using [[COM:MOA|Commons Mobile App]]"; - - public static final String FEEDBACK_EMAIL = "commons-app-android@googlegroups.com"; - - public static final String FEEDBACK_EMAIL_SUBJECT = "Commons Android App Feedback"; - - public static final String REPORT_EMAIL = "commons-app-android-private@googlegroups.com"; - - public static final String REPORT_EMAIL_SUBJECT = "Report a violation"; - - public static final String NOTIFICATION_CHANNEL_ID_ALL = "CommonsNotificationAll"; - - public static final String FEEDBACK_EMAIL_TEMPLATE_HEADER = "-- Technical information --"; - - /** - * Constants End - */ - - private static CommonsApplication INSTANCE; - - public static CommonsApplication getInstance() { - return INSTANCE; - } - - private AppLanguageLookUpTable languageLookUpTable; - - public AppLanguageLookUpTable getLanguageLookUpTable() { - return languageLookUpTable; - } - - @Inject - ContributionDao contributionDao; - - public static Boolean isPaused = false; - - /** - * Used to declare and initialize various components and dependencies - */ - @Override - public void onCreate() { - super.onCreate(); - - INSTANCE = this; - ACRA.init(this); - - ApplicationlessInjection - .getInstance(this) - .getCommonsApplicationComponent() - .inject(this); - - initTimber(); - - if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { - Set defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS); - if (null == defaultExifTagsSet) { - defaultExifTagsSet = new HashSet<>(); - } - defaultExifTagsSet.add(getString(R.string.exif_tag_location)); - defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet); - } - -// Set DownsampleEnabled to True to downsample the image in case it's heavy - ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this) - .setNetworkFetcher(customOkHttpNetworkFetcher) - .setDownsampleEnabled(true) - .build(); - try { - Fresco.initialize(this, config); - } catch (Exception e) { - Timber.e(e); - // TODO: Remove when we're able to initialize Fresco in test builds. - } - - createNotificationChannel(this); - - languageLookUpTable = new AppLanguageLookUpTable(this); - - // This handler will catch exceptions thrown from Observables after they are disposed, - // or from Observables that are (deliberately or not) missing an onError handler. - RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()); - - // Fire progress callbacks for every 3% of uploaded content - System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0"); - } - - /** - * Plants debug and file logging tree. Timber lets you plant your own logging trees. - */ - private void initTimber() { - boolean isBeta = ConfigUtils.isBetaFlavour(); - String logFileName = - isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs"; - String logDirectory = LogUtils.getLogDirectory(); - //Delete stale logs if they have exceeded the specified size - deleteStaleLogs(logFileName, logDirectory); - - FileLoggingTree tree = new FileLoggingTree( - Log.VERBOSE, - logFileName, - logDirectory, - 1000, - getFileLoggingThreadPool()); - - Timber.plant(tree); - Timber.plant(new Timber.DebugTree()); - } - - /** - * Deletes the logs zip file at the specified directory and file locations specified in the - * params - * - * @param logFileName - * @param logDirectory - */ - private void deleteStaleLogs(String logFileName, String logDirectory) { - try { - File file = new File(logDirectory + "/zip/" + logFileName + ".zip"); - if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs - file.delete(); - } - } catch (Exception e) { - Timber.e(e); - } - } - - public static boolean isRoboUnitTest() { - return "robolectric".equals(Build.FINGERPRINT); - } - - private ThreadPoolService getFileLoggingThreadPool() { - return new ThreadPoolService.Builder("file-logging-thread") - .setPriority(Process.THREAD_PRIORITY_LOWEST) - .setPoolSize(1) - .setExceptionHandler(new BackgroundPoolExceptionHandler()) - .build(); - } - - public static void createNotificationChannel(@NonNull Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationManager manager = (NotificationManager) context - .getSystemService(Context.NOTIFICATION_SERVICE); - NotificationChannel channel = manager - .getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL); - if (channel == null) { - channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL, - context.getString(R.string.notifications_channel_name_all), - NotificationManager.IMPORTANCE_DEFAULT); - manager.createNotificationChannel(channel); - } - } - } - - public String getUserAgent() { - return "Commons/" + ConfigUtils.getVersionNameWithSha(this) - + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE; - } - - /** - * clears data of current application - * - * @param context Application context - * @param logoutListener Implementation of interface LogoutListener - */ - @SuppressLint("CheckResult") - public void clearApplicationData(Context context, LogoutListener logoutListener) { - File cacheDirectory = context.getCacheDir(); - File applicationDirectory = new File(cacheDirectory.getParent()); - if (applicationDirectory.exists()) { - String[] fileNames = applicationDirectory.list(); - for (String fileName : fileNames) { - if (!fileName.equals("lib")) { - FileUtils.deleteFile(new File(applicationDirectory, fileName)); - } - } - } - - sessionManager.logout() - .andThen(Completable.fromAction(() -> cookieJar.clear())) - .andThen(Completable.fromAction(() -> { - Timber.d("All accounts have been removed"); - clearImageCache(); - //TODO: fix preference manager - defaultPrefs.clearAll(); - defaultPrefs.putBoolean("firstrun", false); - updateAllDatabases(); - } - )) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(logoutListener::onLogoutComplete, Timber::e); - } - - /** - * Clear all images cache held by Fresco - */ - private void clearImageCache() { - ImagePipeline imagePipeline = Fresco.getImagePipeline(); - imagePipeline.clearCaches(); - } - - /** - * Deletes all tables and re-creates them. - */ - private void updateAllDatabases() { - dbOpenHelper.getReadableDatabase().close(); - SQLiteDatabase db = dbOpenHelper.getWritableDatabase(); - - CategoryDao.Table.onDelete(db); - dbOpenHelper.deleteTable(db, - CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions - - try { - contributionDao.deleteAll(); - } catch (SQLiteException e) { - Timber.e(e); - } - BookmarkPicturesDao.Table.onDelete(db); - BookmarkLocationsDao.Table.onDelete(db); - Table.onDelete(db); - } - - - /** - * Interface used to get log-out events - */ - public interface LogoutListener { - - void onLogoutComplete(); - } - - /** - * This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity - * with relevant intent parameters. It does not perform the actual logout operation. - */ - public static class BaseLogoutListener implements CommonsApplication.LogoutListener { - - Context ctx; - String loginMessage, userName; - - /** - * Constructor for BaseLogoutListener. - * - * @param ctx Application context - */ - public BaseLogoutListener(final Context ctx) { - this.ctx = ctx; - } - - /** - * Constructor for BaseLogoutListener - * - * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. - * @param loginMessage Message to be displayed on the login page - * @param loginUsername Username to be pre-filled on the login page - */ - public BaseLogoutListener(final Context ctx, final String loginMessage, - final String loginUsername) { - this.ctx = ctx; - this.loginMessage = loginMessage; - this.userName = loginUsername; - } - - @Override - public void onLogoutComplete() { - Timber.d("Logout complete callback received."); - final Intent loginIntent = new Intent(ctx, LoginActivity.class); - loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - if (loginMessage != null) { - loginIntent.putExtra(loginMessageIntentKey, loginMessage); - } - if (userName != null) { - loginIntent.putExtra(loginUsernameIntentKey, userName); - } - - ctx.startActivity(loginIntent); - } - } - - /** - * This class is an extension of BaseLogoutListener, providing additional functionality or customization - * for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen. - */ - public static class ActivityLogoutListener extends BaseLogoutListener { - - Activity activity; - - - /** - * Constructor for ActivityLogoutListener. - * - * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. - * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. - */ - public ActivityLogoutListener(final Activity activity, final Context ctx) { - super(ctx); - this.activity = activity; - } - - /** - * Constructor for ActivityLogoutListener with additional parameters for the login screen. - * - * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. - * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. - * @param loginMessage Message to be displayed on the login page after logout. - * @param loginUsername Username to be pre-filled on the login page after logout. - */ - public ActivityLogoutListener(final Activity activity, final Context ctx, - final String loginMessage, final String loginUsername) { - super(activity, loginMessage, loginUsername); - this.activity = activity; - } - - @Override - public void onLogoutComplete() { - super.onLogoutComplete(); - activity.finish(); - } - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt new file mode 100644 index 0000000000..9ed19d6867 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.kt @@ -0,0 +1,414 @@ +package fr.free.nrw.commons + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.database.sqlite.SQLiteException +import android.os.Build +import android.os.Process +import android.util.Log +import androidx.multidex.MultiDexApplication +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.core.ImagePipelineConfig +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao +import fr.free.nrw.commons.category.CategoryDao +import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler +import fr.free.nrw.commons.concurrency.ThreadPoolService +import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.language.AppLanguageLookUpTable +import fr.free.nrw.commons.logging.FileLoggingTree +import fr.free.nrw.commons.logging.LogUtils +import fr.free.nrw.commons.media.CustomOkHttpNetworkFetcher +import fr.free.nrw.commons.settings.Prefs +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import io.reactivex.Completable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.internal.functions.Functions +import io.reactivex.plugins.RxJavaPlugins +import io.reactivex.schedulers.Schedulers +import org.acra.ACRA.init +import org.acra.ReportField +import org.acra.annotation.AcraCore +import org.acra.annotation.AcraDialog +import org.acra.annotation.AcraMailSender +import org.acra.data.StringFormat +import timber.log.Timber +import timber.log.Timber.DebugTree +import java.io.File +import javax.inject.Inject +import javax.inject.Named + +@AcraCore( + buildConfigClass = BuildConfig::class, + resReportSendSuccessToast = R.string.crash_dialog_ok_toast, + reportFormat = StringFormat.KEY_VALUE_LIST, + reportContent = [ReportField.USER_COMMENT, ReportField.APP_VERSION_CODE, ReportField.APP_VERSION_NAME, ReportField.ANDROID_VERSION, ReportField.PHONE_MODEL, ReportField.STACK_TRACE] +) + +@AcraMailSender(mailTo = "commons-app-android-private@googlegroups.com", reportAsFile = false) + +@AcraDialog( + resTheme = R.style.Theme_AppCompat_Dialog, + resText = R.string.crash_dialog_text, + resTitle = R.string.crash_dialog_title, + resCommentPrompt = R.string.crash_dialog_comment_prompt +) + +class CommonsApplication : MultiDexApplication() { + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var dbOpenHelper: DBOpenHelper + + @Inject + @field:Named("default_preferences") + lateinit var defaultPrefs: JsonKvStore + + @Inject + lateinit var cookieJar: CommonsCookieJar + + @Inject + lateinit var customOkHttpNetworkFetcher: CustomOkHttpNetworkFetcher + + var languageLookUpTable: AppLanguageLookUpTable? = null + private set + + @Inject + lateinit var contributionDao: ContributionDao + + /** + * Used to declare and initialize various components and dependencies + */ + override fun onCreate() { + super.onCreate() + + instance = this + init(this) + + ApplicationlessInjection + .getInstance(this) + .commonsApplicationComponent + .inject(this) + + initTimber() + + if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) { + var defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS) + if (null == defaultExifTagsSet) { + defaultExifTagsSet = HashSet() + } + defaultExifTagsSet.add(getString(R.string.exif_tag_location)) + defaultPrefs.putStringSet(Prefs.MANAGED_EXIF_TAGS, defaultExifTagsSet) + } + + // Set DownsampleEnabled to True to downsample the image in case it's heavy + val config = ImagePipelineConfig.newBuilder(this) + .setNetworkFetcher(customOkHttpNetworkFetcher) + .setDownsampleEnabled(true) + .build() + try { + Fresco.initialize(this, config) + } catch (e: Exception) { + Timber.e(e) + // TODO: Remove when we're able to initialize Fresco in test builds. + } + + createNotificationChannel(this) + + languageLookUpTable = AppLanguageLookUpTable(this) + + // This handler will catch exceptions thrown from Observables after they are disposed, + // or from Observables that are (deliberately or not) missing an onError handler. + RxJavaPlugins.setErrorHandler(Functions.emptyConsumer()) + + // Fire progress callbacks for every 3% of uploaded content + System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0") + } + + /** + * Plants debug and file logging tree. Timber lets you plant your own logging trees. + */ + private fun initTimber() { + val isBeta = isBetaFlavour + val logFileName = + if (isBeta) "CommonsBetaAppLogs" else "CommonsAppLogs" + val logDirectory = LogUtils.getLogDirectory() + //Delete stale logs if they have exceeded the specified size + deleteStaleLogs(logFileName, logDirectory) + + val tree = FileLoggingTree( + Log.VERBOSE, + logFileName, + logDirectory, + 1000, + fileLoggingThreadPool + ) + + Timber.plant(tree) + Timber.plant(DebugTree()) + } + + /** + * Deletes the logs zip file at the specified directory and file locations specified in the + * params + * + * @param logFileName + * @param logDirectory + */ + private fun deleteStaleLogs(logFileName: String, logDirectory: String) { + try { + val file = File("$logDirectory/zip/$logFileName.zip") + if (file.exists() && file.totalSpace > 1000000) { // In Kbs + file.delete() + } + } catch (e: Exception) { + Timber.e(e) + } + } + + private val fileLoggingThreadPool: ThreadPoolService + get() = ThreadPoolService.Builder("file-logging-thread") + .setPriority(Process.THREAD_PRIORITY_LOWEST) + .setPoolSize(1) + .setExceptionHandler(BackgroundPoolExceptionHandler()) + .build() + + val userAgent: String + get() = ("Commons/" + this.getVersionNameWithSha() + + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE) + + /** + * clears data of current application + * + * @param context Application context + * @param logoutListener Implementation of interface LogoutListener + */ + @SuppressLint("CheckResult") + fun clearApplicationData(context: Context, logoutListener: LogoutListener) { + val cacheDirectory = context.cacheDir + val applicationDirectory = File(cacheDirectory.parent) + if (applicationDirectory.exists()) { + val fileNames = applicationDirectory.list() + for (fileName in fileNames) { + if (fileName != "lib") { + FileUtils.deleteFile(File(applicationDirectory, fileName)) + } + } + } + + sessionManager.logout() + .andThen(Completable.fromAction { cookieJar.clear() }) + .andThen(Completable.fromAction { + Timber.d("All accounts have been removed") + clearImageCache() + //TODO: fix preference manager + defaultPrefs.clearAll() + defaultPrefs.putBoolean("firstrun", false) + updateAllDatabases() + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ logoutListener.onLogoutComplete() }, { t: Throwable? -> Timber.e(t) }) + } + + /** + * Clear all images cache held by Fresco + */ + private fun clearImageCache() { + val imagePipeline = Fresco.getImagePipeline() + imagePipeline.clearCaches() + } + + /** + * Deletes all tables and re-creates them. + */ + private fun updateAllDatabases() { + dbOpenHelper.readableDatabase.close() + val db = dbOpenHelper.writableDatabase + + CategoryDao.Table.onDelete(db) + dbOpenHelper.deleteTable( + db, + DBOpenHelper.CONTRIBUTIONS_TABLE + ) //Delete the contributions table in the existing db on older versions + + try { + contributionDao.deleteAll() + } catch (e: SQLiteException) { + Timber.e(e) + } + BookmarkPicturesDao.Table.onDelete(db) + BookmarkLocationsDao.Table.onDelete(db) + BookmarkItemsDao.Table.onDelete(db) + } + + + /** + * Interface used to get log-out events + */ + interface LogoutListener { + fun onLogoutComplete() + } + + /** + * This listener is responsible for handling post-logout actions, specifically invoking the LoginActivity + * with relevant intent parameters. It does not perform the actual logout operation. + */ + open class BaseLogoutListener : LogoutListener { + var ctx: Context + var loginMessage: String? = null + var userName: String? = null + + /** + * Constructor for BaseLogoutListener. + * + * @param ctx Application context + */ + constructor(ctx: Context) { + this.ctx = ctx + } + + /** + * Constructor for BaseLogoutListener + * + * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. + * @param loginMessage Message to be displayed on the login page + * @param loginUsername Username to be pre-filled on the login page + */ + constructor( + ctx: Context, loginMessage: String?, + loginUsername: String? + ) { + this.ctx = ctx + this.loginMessage = loginMessage + this.userName = loginUsername + } + + override fun onLogoutComplete() { + Timber.d("Logout complete callback received.") + val loginIntent = Intent(ctx, LoginActivity::class.java) + loginIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (loginMessage != null) { + loginIntent.putExtra(LOGIN_MESSAGE_INTENT_KEY, loginMessage) + } + if (userName != null) { + loginIntent.putExtra(LOGIN_USERNAME_INTENT_KEY, userName) + } + + ctx.startActivity(loginIntent) + } + } + + /** + * This class is an extension of BaseLogoutListener, providing additional functionality or customization + * for the logout process. It includes specific actions to be taken during logout, such as handling redirection to the login screen. + */ + class ActivityLogoutListener : BaseLogoutListener { + var activity: Activity + + + /** + * Constructor for ActivityLogoutListener. + * + * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. + * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. + */ + constructor(activity: Activity, ctx: Context) : super(ctx) { + this.activity = activity + } + + /** + * Constructor for ActivityLogoutListener with additional parameters for the login screen. + * + * @param activity The activity context from which the logout is initiated. Used to perform actions such as finishing the activity. + * @param ctx The application context, used for invoking the LoginActivity and passing relevant intent parameters as part of the post-logout process. + * @param loginMessage Message to be displayed on the login page after logout. + * @param loginUsername Username to be pre-filled on the login page after logout. + */ + constructor( + activity: Activity, ctx: Context?, + loginMessage: String?, loginUsername: String? + ) : super(activity, loginMessage, loginUsername) { + this.activity = activity + } + + override fun onLogoutComplete() { + super.onLogoutComplete() + activity.finish() + } + } + + companion object { + + const val LOGIN_MESSAGE_INTENT_KEY: String = "loginMessage" + const val LOGIN_USERNAME_INTENT_KEY: String = "loginUsername" + + const val IS_LIMITED_CONNECTION_MODE_ENABLED: String = "is_limited_connection_mode_enabled" + + /** + * Constants begin + */ + const val OPEN_APPLICATION_DETAIL_SETTINGS: Int = 1001 + + const val DEFAULT_EDIT_SUMMARY: String = "Uploaded using [[COM:MOA|Commons Mobile App]]" + + const val FEEDBACK_EMAIL: String = "commons-app-android@googlegroups.com" + + const val FEEDBACK_EMAIL_SUBJECT: String = "Commons Android App Feedback" + + const val REPORT_EMAIL: String = "commons-app-android-private@googlegroups.com" + + const val REPORT_EMAIL_SUBJECT: String = "Report a violation" + + const val NOTIFICATION_CHANNEL_ID_ALL: String = "CommonsNotificationAll" + + const val FEEDBACK_EMAIL_TEMPLATE_HEADER: String = "-- Technical information --" + + /** + * Constants End + */ + + @JvmStatic + lateinit var instance: CommonsApplication + private set + + @JvmField + var isPaused: Boolean = false + + @JvmStatic + fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = context + .getSystemService(NOTIFICATION_SERVICE) as NotificationManager + var channel = manager + .getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL) + if (channel == null) { + channel = NotificationChannel( + NOTIFICATION_CHANNEL_ID_ALL, + context.getString(R.string.notifications_channel_name_all), + NotificationManager.IMPORTANCE_DEFAULT + ) + manager.createNotificationChannel(channel) + } + } + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt index de716db99e..af305c9c6a 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -32,7 +32,7 @@ class ThanksClient revisionId.toString(), // Rev null, // Log csrfTokenClient.getTokenBlocking(), // Token - CommonsApplication.getInstance().userAgent, // Source + CommonsApplication.instance.userAgent, // Source ).map { mwThankPostResponse -> mwThankPostResponse.result?.success == 1 } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 0b6d1831c5..3ff61e511c 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -50,8 +50,8 @@ import static android.view.KeyEvent.KEYCODE_ENTER; import static android.view.View.VISIBLE; import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.CommonsApplication.loginMessageIntentKey; -import static fr.free.nrw.commons.CommonsApplication.loginUsernameIntentKey; +import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY; +import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY; public class LoginActivity extends AccountAuthenticatorActivity { @@ -94,8 +94,8 @@ public void onCreate(Bundle savedInstanceState) { binding = ActivityLoginBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); - String message = getIntent().getStringExtra(loginMessageIntentKey); - String username = getIntent().getStringExtra(loginUsernameIntentKey); + String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY); + String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY); binding.loginUsername.addTextChangedListener(textWatcher); binding.loginPassword.addTextChangedListener(textWatcher); diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index cfd7f36b9a..7ed598637b 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -258,7 +258,7 @@ class DescriptionEditActivity : username, ) - val commonsApplication = CommonsApplication.getInstance() + val commonsApplication = CommonsApplication.instance if (commonsApplication != null) { commonsApplication.clearApplicationData(this, logoutListener) } @@ -291,7 +291,7 @@ class DescriptionEditActivity : username, ) - val commonsApplication = CommonsApplication.getInstance() + val commonsApplication = CommonsApplication.instance if (commonsApplication != null) { commonsApplication.clearApplicationData(this, logoutListener) } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index fb2ca7b3ac..15a0494892 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -438,7 +438,7 @@ class UploadWorker( username, ) CommonsApplication - .getInstance() + .instance!! .clearApplicationData(appContext, logoutListener) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt index d409016ae2..b3fb19c104 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/actions/ThanksClientTest.kt @@ -4,6 +4,8 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.verify import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.auth.csrf.CsrfTokenClient +import io.mockk.every +import io.mockk.mockkObject import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,7 +31,6 @@ class ThanksClientTest { private lateinit var commonsApplication: CommonsApplication private lateinit var thanksClient: ThanksClient - private lateinit var mockedApplication: MockedStatic /** * initial setup, test environment @@ -38,8 +39,8 @@ class ThanksClientTest { @Throws(Exception::class) fun setUp() { MockitoAnnotations.openMocks(this) - mockedApplication = Mockito.mockStatic(CommonsApplication::class.java) - `when`(CommonsApplication.getInstance()).thenReturn(commonsApplication) + mockkObject(CommonsApplication) + every { CommonsApplication.instance }.returns(commonsApplication) thanksClient = ThanksClient(csrfTokenClient, service) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt index 00f438e1e3..be3b7e8e3c 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/description/DescriptionEditActivityUnitTest.kt @@ -10,6 +10,7 @@ import android.os.Looper import android.view.LayoutInflater import android.view.View import androidx.recyclerview.widget.RecyclerView +import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.Media import fr.free.nrw.commons.R import fr.free.nrw.commons.TestCommonsApplication @@ -19,6 +20,8 @@ import fr.free.nrw.commons.description.EditDescriptionConstants.WIKITEXT import fr.free.nrw.commons.settings.Prefs import fr.free.nrw.commons.upload.UploadMediaDetail import fr.free.nrw.commons.upload.UploadMediaDetailAdapter +import io.mockk.every +import io.mockk.mockkObject import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before @@ -54,6 +57,9 @@ class DescriptionEditActivityUnitTest { @Mock private lateinit var rvDescriptions: RecyclerView + @Mock + private lateinit var commonsApplication: CommonsApplication + private lateinit var media: Media @Before @@ -82,6 +88,8 @@ class DescriptionEditActivityUnitTest { bundle.putString(Prefs.DESCRIPTION_LANGUAGE, "bn") bundle.putParcelable("media", media) intent.putExtras(bundle) + mockkObject(CommonsApplication) + every { CommonsApplication.instance }.returns(commonsApplication) activity = Robolectric.buildActivity(DescriptionEditActivity::class.java, intent).create().get() binding = ActivityDescriptionEditBinding.inflate(LayoutInflater.from(activity)) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt index 97aac88fe6..50130106a0 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadClientTest.kt @@ -10,7 +10,7 @@ import com.nhaarman.mockitokotlin2.eq import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.times import com.nhaarman.mockitokotlin2.whenever -import fr.free.nrw.commons.CommonsApplication.DEFAULT_EDIT_SUMMARY +import fr.free.nrw.commons.CommonsApplication.Companion.DEFAULT_EDIT_SUMMARY import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.contributions.ChunkInfo import fr.free.nrw.commons.contributions.Contribution From 610919ec677358ff4837469b284410dc7a9afc35 Mon Sep 17 00:00:00 2001 From: u7119288 <141695960+baijun6@users.noreply.github.com> Date: Mon, 28 Oct 2024 00:59:09 +1100 Subject: [PATCH 005/231] Partial fixes for errors and warnings reported by ./gradlew lint (#5885) * BaseMarker.kt: removed unneeded cast * TransformImageImpl.kt: removed unreachable code * ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView * ZoomableActivity.kt: removed Unnecessary safe call on a non-null receiver of type ZoomableDraweeView * DescriptionEditActivity.kt: removed unnecessary non-null assertion (!!) on a non-null receiver of type DescriptionEditHelper * Media.kt: Property would not be serialized into a 'Parcel'. Added '@IgnoredOnParcel' annotation to remove the warning * ZoomableActivity.kt: removed Unnecessary non-null assertion (!!) on a non-null receiver of type ZoomableDraweeView * CategoryClient.kt: removed condition 'page.categoryInfo() == null' as it's always 'false' * DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type SessionManager * DescriptionEditActivity.kt: removed unnecessary safe call on a non-null receiver of type DescriptionEditHelper * WikidataFeedback.kt: removed unneeded cast * FailedUploadsFragment.kt: removed unneeded non-null assertion (!!) * PendingUploadsFragment.kt: removed unneeded non-null assertion (!!) * AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * FileUtils.java: Changed toUpperCase to toUpperCase(Locale.getDefault()) * AchievementsFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ExploreFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * LocationPickerActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * MediaDetailFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * NearbyFilterSearchRecyclerViewAdapter.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ProfileActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * RecentSearchesFragment.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * ReviewActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * SearchActivity.java: Changed toUpperCase to toUpperCase(Locale.ROOT) * UploadMediaPresenter.java: Changed toUpperCase to toLowerCase(Locale.ROOT) * CategoriesMediaFragment.kt: Changed arguments!! to requireArguments() * ChildDepictionsFragment.kt: Changed arguments!! to requireArguments() * DepictedImagesFragment.kt: Changed arguments!! to requireArguments() * DepictsFragment: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity())) to requireActivity() * ParentCategoriesFragment.kt: Changed arguments!! to requireArguments() * ParentCategoriesFragment.kt: Changed arguments!! to requireArguments() * SubCategoriesFragment.kt: Changed arguments!! to requireArguments() * SubCategoriesFragment.kt: Changed Objects.requireNonNull(getView()) to requireViews(), Objects.requireNonNull(getActivity()) to requireActivity() * UploadMediaDetailFragment.java: Changed arguments!! to requireArguments() * WikipediaInstructionsDialogFragment.kt: Changed arguments!! to requireArguments() * BookmarkItemsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex() * BookmarkLocationsDao.java: Added @SuppressLint("Range"), as -1 is expected behavior not index doesn't exist for getColumnIndex() * AndroidManifest.xml: Removed redundant label android:label="@string/app_name" * bs\strings.xml: Added missing few quantity * hr\strings.xml: Added missing few quantity * hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63 * Revert "hr\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63" This reverts commit 47232466ab4fc3ac91958f888bc5033fefc699a5. * cy\strings.xml: Added missing zero, two, few, many quantities for lines 23 - 63 * sr\strings.xml: Added missing few quantities for lines 35 to 70 * ro\strings.xml: Added missing few quantity and removed not needed zero for * cs\strings.xml: Added missing few many missing quantities for lines 33 - 74 * lt\strings.xml: Added missing few, many missing quantities for lines 34 - 58 * lt\strings.xml: Replaced . . . with ... * ca\strings.xml: Added missing many quantities for lines 21 - 51, replaced . . . with ... * ser\strings.xml: Added missing few quantities, replaced . . . with ... * br\strings.xml: Added missing two, few, many quantities * pt\strings.xml: Added missing many quantity, changed . . . to ... and ignored typo as it is correct for European Portuguese * it\strings.xml: changed . . . to ... * pt\strings.xml: fixed many quantity * ca\strings.xml: fixed many quantity * sr\strings.xml: fixed many quantity * cy\strings.xml: corrected quantities for "share_license_summary * fr\strings.xml: changed . . . to ... and add many quantities using the other quantity * fr\strings.xml: changed . . . to ... and add many quantities using the other quantity. Fixed some typos, added ignore for correct spellings but has warning * getColumnIndex(): added @SuppressLint("Range") as -1 is expected result for column name doesn't exist * values-b+sr+Latn\strings.xml: changed . . . to ... * Revert "values-b+sr+Latn\strings.xml: changed . . . to ..." This reverts commit 95b909c29f7f96fe0a885a22daa7fd1a3568e058. * values-b+roa+tara\strings.xml: changed . . . to ... * Revert "values-b+roa+tara\strings.xml: changed . . . to ..." This reverts commit b5db1a3e68a9abfb95b4fc3f6219b507ead16d16. * values-b+roa+tara\strings.xml: changed . . . to ... * values-b+sr+Latn\strings.xml: changed . . . to ..., add few based on other quantity. Ignored one ImpliedQuantity warning as it is correct. * it\strings.xml: changed . . . to ..., add many based on other quantity. * pt-rBR\strings.xml: changed . . . to ..., add many based on other quantity. Fixed typos, ignored warning for "one" quantity as translation didn't use number * si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity * si\strings.xml: Ignored ImpliedQuantity warning as it uses 1 not %d. Removed not needed zero quantity. Fixed wrong %1$d. Changed . . . to ... * mk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use 1 * sl\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * ru\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * uk\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * is\strings.xml: changed . . . to ... and ignored ImpliedQuantity as it doesn't use %1$d * strings.xml: changed . . . to ... * af\strings.xml: removed not needed zero quantity * de\strings.xml: fixed duplicate word typo * diq\strings.xml: changed - to dash (-) * hi\strings.xml: removed not needed zero * in\strings.xml: removed not needed one quantity * iw\strings.xml: removed not needed many quantity * ja\strings.xml: removed not needed one quantity * ko\strings.xml: removed not needed one quantity * ky\strings.xml: removed not needed one quantity * mr\strings.xml: removed not needed zero quantity * my\strings.xml: removed not needed one quantity * su\strings.xml: removed not needed one quantity * th\strings.xml: removed not needed one and zero quantity * zh\strings.xml: removed not needed one quantity * activity_description_edit.xml: changed android:tint to app:tint, changed layout_alignParentRight to layout_alignParentEnd * bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed to margin layout * bottom_sheet_item_layout.xml: changed android:tint to app:tint, added focusable * bottom_sheet_details_explore.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * item_place.xml.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * layout_campagin.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * layout_contribution.xml.xml: changed android:tint to app:tint * nearby_card_view.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * nearby_row_button.xml: changed android:tint to app:tint, added focusable, changed margin layout and removed not needed and invalid params * toolbar_location_picker.xml: changed android:tint to app:tint --------- Co-authored-by: Nicolas Raoul --- app/src/main/AndroidManifest.xml | 3 +- .../java/fr/free/nrw/commons/BaseMarker.kt | 2 +- .../LocationPickerActivity.java | 4 +- .../main/java/fr/free/nrw/commons/Media.kt | 2 + .../bookmarks/items/BookmarkItemsDao.java | 2 + .../locations/BookmarkLocationsDao.java | 2 + .../pictures/BookmarkPicturesDao.java | 2 + .../nrw/commons/category/CategoryClient.kt | 2 +- .../nrw/commons/category/CategoryDao.java | 2 + .../WikipediaInstructionsDialogFragment.kt | 2 +- .../description/DescriptionEditActivity.kt | 6 +- .../nrw/commons/edit/TransformImageImpl.kt | 1 - .../nrw/commons/explore/ExploreFragment.java | 7 +- .../nrw/commons/explore/SearchActivity.java | 7 +- .../media/CategoriesMediaFragment.kt | 2 +- .../parent/ParentCategoriesFragment.kt | 2 +- .../categories/sub/SubCategoriesFragment.kt | 2 +- .../child/ChildDepictionsFragment.kt | 4 +- .../media/DepictedImagesFragment.kt | 2 +- .../parent/ParentDepictionsFragment.kt | 4 +- .../recentsearches/RecentSearchesDao.java | 2 + .../RecentSearchesFragment.java | 3 +- .../commons/media/MediaDetailFragment.java | 4 +- .../nrw/commons/media/ZoomableActivity.kt | 18 ++--- ...NearbyFilterSearchRecyclerViewAdapter.java | 5 +- .../nrw/commons/nearby/WikidataFeedback.kt | 2 +- .../nrw/commons/profile/ProfileActivity.java | 5 +- .../achievements/AchievementsFragment.java | 3 +- .../recentlanguages/RecentLanguagesDao.java | 2 + .../nrw/commons/review/ReviewActivity.java | 3 +- .../commons/upload/FailedUploadsFragment.kt | 6 +- .../fr/free/nrw/commons/upload/FileUtils.java | 3 +- .../commons/upload/PendingUploadsFragment.kt | 4 +- .../categories/UploadCategoriesFragment.java | 6 +- .../upload/depicts/DepictsFragment.java | 6 +- .../UploadMediaDetailFragment.java | 2 +- .../mediaDetails/UploadMediaPresenter.java | 4 +- .../res/layout/activity_description_edit.xml | 6 +- .../layout/bottom_sheet_details_explore.xml | 12 ++-- .../res/layout/bottom_sheet_item_layout.xml | 4 +- .../main/res/layout/fragment_achievements.xml | 67 +++++-------------- app/src/main/res/layout/item_place.xml | 13 +--- app/src/main/res/layout/layout_campagin.xml | 19 ++---- .../main/res/layout/layout_contribution.xml | 4 +- app/src/main/res/layout/nearby_card_view.xml | 16 ++--- app/src/main/res/layout/nearby_row_button.xml | 20 +++--- .../res/layout/toolbar_location_picker.xml | 4 +- app/src/main/res/values-ab/strings.xml | 4 +- app/src/main/res/values-af/strings.xml | 3 +- app/src/main/res/values-anp/strings.xml | 10 +-- app/src/main/res/values-ar/strings.xml | 8 +-- app/src/main/res/values-as/strings.xml | 4 +- app/src/main/res/values-ast/strings.xml | 4 +- app/src/main/res/values-az/strings.xml | 2 +- .../main/res/values-b+roa+tara/strings.xml | 6 +- app/src/main/res/values-b+sr+Latn/strings.xml | 19 ++++-- app/src/main/res/values-ba/strings.xml | 8 +-- app/src/main/res/values-ban/strings.xml | 6 +- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-blk/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 6 ++ app/src/main/res/values-bs/strings.xml | 5 +- app/src/main/res/values-ca/strings.xml | 8 ++- app/src/main/res/values-ce/strings.xml | 8 +-- app/src/main/res/values-cs/strings.xml | 15 ++++- app/src/main/res/values-csb/strings.xml | 6 +- app/src/main/res/values-cy/strings.xml | 19 ++++++ app/src/main/res/values-da/strings.xml | 10 +-- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-diq/strings.xml | 8 +-- app/src/main/res/values-el/strings.xml | 8 +-- app/src/main/res/values-eo/strings.xml | 16 ++--- app/src/main/res/values-es/strings.xml | 38 +++++++---- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 10 +-- app/src/main/res/values-fi/strings.xml | 10 +-- app/src/main/res/values-fr/strings.xml | 36 ++++++---- app/src/main/res/values-gcr/strings.xml | 6 +- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 7 +- app/src/main/res/values-hr/strings.xml | 17 +++-- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 18 +++-- app/src/main/res/values-io/strings.xml | 14 ++-- app/src/main/res/values-is/strings.xml | 8 +-- app/src/main/res/values-it/strings.xml | 15 ++++- app/src/main/res/values-iw/strings.xml | 26 +++---- app/src/main/res/values-ja/strings.xml | 5 +- app/src/main/res/values-kab/strings.xml | 8 +-- app/src/main/res/values-ko/strings.xml | 14 ++-- app/src/main/res/values-krc/strings.xml | 12 ++-- app/src/main/res/values-ku/strings.xml | 6 +- app/src/main/res/values-kum/strings.xml | 2 +- app/src/main/res/values-kus/strings.xml | 18 ++--- app/src/main/res/values-ky/strings.xml | 3 +- app/src/main/res/values-lb/strings.xml | 10 +-- app/src/main/res/values-li/strings.xml | 8 +-- app/src/main/res/values-lt/strings.xml | 21 ++++-- app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 12 ++-- app/src/main/res/values-mni/strings.xml | 4 +- app/src/main/res/values-mnw/strings.xml | 4 +- app/src/main/res/values-mr/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 14 ++-- app/src/main/res/values-nl/strings.xml | 6 +- app/src/main/res/values-nqo/strings.xml | 20 +++--- app/src/main/res/values-oc/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 13 ++-- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 6 +- app/src/main/res/values-ps/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++--- app/src/main/res/values-pt/strings.xml | 32 ++++++--- app/src/main/res/values-ro/strings.xml | 10 +-- app/src/main/res/values-ru/strings.xml | 14 ++-- app/src/main/res/values-sd/strings.xml | 4 +- app/src/main/res/values-se/strings.xml | 8 +-- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-si/strings.xml | 11 ++- app/src/main/res/values-sk/strings.xml | 14 ++-- app/src/main/res/values-sl/strings.xml | 30 ++++----- app/src/main/res/values-sr/strings.xml | 17 +++-- app/src/main/res/values-su/strings.xml | 13 +--- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tcy/strings.xml | 4 +- app/src/main/res/values-te/strings.xml | 12 ++-- app/src/main/res/values-th/strings.xml | 7 +- app/src/main/res/values-tr/strings.xml | 6 +- app/src/main/res/values-uk/strings.xml | 6 +- app/src/main/res/values-uz/strings.xml | 8 +-- app/src/main/res/values-vec/strings.xml | 10 +-- app/src/main/res/values-xal/strings.xml | 8 +-- app/src/main/res/values-xmf/strings.xml | 6 +- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 17 ++--- 137 files changed, 617 insertions(+), 572 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89ed630d84..29f280c9ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -99,7 +99,6 @@ android:exported="true" android:hardwareAccelerated="false" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" android:windowSoftInputMode="adjustResize"> @@ -122,7 +121,7 @@ android:name=".contributions.MainActivity" android:configChanges="screenSize|keyboard|orientation" android:icon="@mipmap/ic_launcher" - android:label="@string/app_name" /> + /> diff --git a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt index 1daadb5a16..28b01d6031 100644 --- a/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt +++ b/app/src/main/java/fr/free/nrw/commons/BaseMarker.kt @@ -46,7 +46,7 @@ class BaseMarker { val drawable: Drawable = context.resources.getDrawable(drawableResId) icon = if (drawable is BitmapDrawable) { - (drawable as BitmapDrawable).bitmap + drawable.bitmap } else { val bitmap = Bitmap.createBitmap( diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java index 8c54fd292b..2f05705bac 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java @@ -53,6 +53,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; import org.osmdroid.tileprovider.tilesource.TileSourceFactory; @@ -301,7 +302,8 @@ private void bindViews() { modifyLocationButton = findViewById(R.id.modify_location); removeLocationButton = findViewById(R.id.remove_location); showInMapButton = findViewById(R.id.show_in_map); - showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase()); + showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase( + Locale.ROOT)); shadow = findViewById(R.id.location_picker_image_view_shadow); } 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 93efac7b23..025302cfdb 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.kt +++ b/app/src/main/java/fr/free/nrw/commons/Media.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons import android.os.Parcelable import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.wikidata.model.page.PageTitle +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import java.util.Date import java.util.Locale @@ -124,6 +125,7 @@ class Media constructor( * Gets the categories the file falls under. * @return file categories as an ArrayList of Strings */ + @IgnoredOnParcel var addedCategories: List? = null // TODO added categories should be removed. It is added for a short fix. On category update, // categories should be re-fetched instead diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java index 70c3708368..6788a8290b 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/items/BookmarkItemsDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.items; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -134,6 +135,7 @@ public boolean findBookmarkItem(final String depictedItemID) { * @param cursor : Object for storing database data * @return DepictedItem */ + @SuppressLint("Range") DepictedItem fromCursor(final Cursor cursor) { final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); final String description diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java index 850b953e94..fe4f603f49 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/locations/BookmarkLocationsDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.locations; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -146,6 +147,7 @@ public boolean findBookmarkLocation(Place bookmarkLocation) { return false; } + @SuppressLint("Range") @NonNull Place fromCursor(final Cursor cursor) { final LatLng location = new LatLng(cursor.getDouble(cursor.getColumnIndex(Table.COLUMN_LAT)), diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java index a56a39ba20..c214ae996e 100644 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/pictures/BookmarkPicturesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.bookmarks.pictures; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -150,6 +151,7 @@ public boolean findBookmark(Bookmark bookmark) { return false; } + @SuppressLint("Range") @NonNull Bookmark fromCursor(Cursor cursor) { String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME)); diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 64463d8263..992c4ed1cf 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -124,7 +124,7 @@ class CategoryClient }.map { it .filter { page -> - page.categoryInfo() == null || !page.categoryInfo().isHidden + !page.categoryInfo().isHidden }.map { CategoryItem( it.title().replace(CATEGORY_PREFIX, ""), diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java index b638fc5081..3cd60ac81a 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.category; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -111,6 +112,7 @@ List recentCategories(int limit) { } @NonNull + @SuppressLint("Range") Category fromCursor(Cursor cursor) { // Hardcoding column positions! return new Category( diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt index 77e52e1dbe..86cda2cf37 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/WikipediaInstructionsDialogFragment.kt @@ -22,7 +22,7 @@ class WikipediaInstructionsDialogFragment : DialogFragment() { ) = DialogAddToWikipediaInstructionsBinding .inflate(inflater, container, false) .apply { - val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION) + val contribution: Contribution? = requireArguments().getParcelable(ARG_CONTRIBUTION) tvWikicode.setText(contribution?.media?.wikiCode) instructionsCancel.setOnClickListener { dismiss() } instructionsConfirm.setOnClickListener { diff --git a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt index 7ed598637b..fa4349dbf4 100644 --- a/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/description/DescriptionEditActivity.kt @@ -237,7 +237,7 @@ class DescriptionEditActivity : ) { try { descriptionEditHelper - ?.addDescription( + .addDescription( applicationContext, media, updatedWikiText, @@ -250,7 +250,7 @@ class DescriptionEditActivity : ) } } catch (e: InvalidLoginTokenException) { - val username: String? = sessionManager?.userName + val username: String? = sessionManager.userName val logoutListener = CommonsApplication.BaseLogoutListener( this, @@ -268,7 +268,7 @@ class DescriptionEditActivity : for (mediaDetail in uploadMediaDetails) { try { compositeDisposable.add( - descriptionEditHelper!! + descriptionEditHelper .addCaption( applicationContext, media, diff --git a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt index b596196915..c3db1a5a0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt +++ b/app/src/main/java/fr/free/nrw/commons/edit/TransformImageImpl.kt @@ -65,7 +65,6 @@ class TransformImageImpl : TransformImage { } catch (e: LLJTranException) { Timber.tag("Error").d(e) return null - false } if (rotated) { 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 c66cd51631..26c8dd82b2 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 @@ -22,6 +22,7 @@ import fr.free.nrw.commons.utils.ActivityUtils; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; @@ -112,13 +113,13 @@ public void setTabs() { mobileRootFragment = new ExploreListRootFragment(mobileArguments); mapRootFragment = new ExploreMapRootFragment(mapArguments); fragmentList.add(featuredRootFragment); - titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_featured).toUpperCase(Locale.ROOT)); fragmentList.add(mobileRootFragment); - titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_mobile).toUpperCase(Locale.ROOT)); fragmentList.add(mapRootFragment); - titleList.add(getString(R.string.explore_tab_title_map).toUpperCase()); + titleList.add(getString(R.string.explore_tab_title_map).toUpperCase(Locale.ROOT)); ((MainActivity)getActivity()).showTabs(); ((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index 7717f2deb7..abb27184f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -28,6 +28,7 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import timber.log.Timber; @@ -95,11 +96,11 @@ public void setTabs() { searchDepictionsFragment = new SearchDepictionsFragment(); searchCategoryFragment= new SearchCategoryFragment(); fragmentList.add(searchMediaFragment); - titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase(Locale.ROOT)); fragmentList.add(searchCategoryFragment); - titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase(Locale.ROOT)); fragmentList.add(searchDepictionsFragment); - titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase()); + titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase(Locale.ROOT)); viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt index 6de1248b4a..765abd698e 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/media/CategoriesMediaFragment.kt @@ -18,6 +18,6 @@ class CategoriesMediaFragment : PageableMediaFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt index 6ceccf6075..c43e1c6bd9 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/parent/ParentCategoriesFragment.kt @@ -21,6 +21,6 @@ class ParentCategoriesFragment : PageableCategoryFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt index 19fe52beb0..8fbc830391 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/sub/SubCategoriesFragment.kt @@ -20,6 +20,6 @@ class SubCategoriesFragment : PageableCategoryFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") + onQueryUpdated("$CATEGORY_PREFIX${requireArguments().getString("categoryName")!!}") } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt index 5275362997..4f13b1be8a 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/child/ChildDepictionsFragment.kt @@ -13,13 +13,13 @@ class ChildDepictionsFragment : PageableDepictionsFragment() { override val injectedPresenter get() = presenter - override fun getEmptyText(query: String) = getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!) + override fun getEmptyText(query: String) = getString(R.string.no_child_classes, requireArguments().getString("wikidataItemName")!!) override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt index cc1b664b2a..4cdb0e4618 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/media/DepictedImagesFragment.kt @@ -17,6 +17,6 @@ class DepictedImagesFragment : PageableMediaFragment() { savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt b/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt index 52a5aff5d8..cf739a07db 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/explore/depictions/parent/ParentDepictionsFragment.kt @@ -13,13 +13,13 @@ class ParentDepictionsFragment : PageableDepictionsFragment() { override val injectedPresenter get() = presenter - override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, arguments!!.getString("wikidataItemName")!!) + override fun getEmptyText(query: String) = getString(R.string.no_parent_classes, requireArguments().getString("wikidataItemName")!!) override fun onViewCreated( view: View, savedInstanceState: Bundle?, ) { super.onViewCreated(view, savedInstanceState) - onQueryUpdated(arguments!!.getString("entityId")!!) + onQueryUpdated(requireArguments().getString("entityId")!!) } } diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java index 9f12639dd2..cee8a25ae8 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.explore.recentsearches; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -178,6 +179,7 @@ public List recentSearches(int limit) { * @return RecentSearch object */ @NonNull + @SuppressLint("Range") RecentSearch fromCursor(Cursor cursor) { // Hardcoding column positions! return new RecentSearch( diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java index cd98651f01..0db1e55395 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java @@ -15,6 +15,7 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.SearchActivity; import java.util.List; +import java.util.Locale; import javax.inject.Inject; @@ -90,7 +91,7 @@ private void setDeleteRecentPositiveButton(@NonNull final Context context, private void showDeleteAlertDialog(@NonNull final Context context, final int position) { new AlertDialog.Builder(context) .setMessage(R.string.delete_search_dialog) - .setPositiveButton(getString(R.string.delete).toUpperCase(), + .setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT), ((dialog, which) -> setDeletePositiveButton(context, dialog, position))) .setNegativeButton(android.R.string.cancel, null) .create() diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index dd0829a1b5..142d8379c7 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -600,8 +600,8 @@ private void updateToDoWarning() { // Check if the presented category is about need of category if (categoriesPresent) { for (String category : media.getCategories()) { - if (category.toLowerCase().contains(CATEGORY_NEEDING_CATEGORIES) || - category.toLowerCase().contains(CATEGORY_UNCATEGORISED)) { + if (category.toLowerCase(Locale.ROOT).contains(CATEGORY_NEEDING_CATEGORIES) || + category.toLowerCase(Locale.ROOT).contains(CATEGORY_UNCATEGORISED)) { categoriesPresent = false; } break; diff --git a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt index d08e3048c3..14b5788c24 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/media/ZoomableActivity.kt @@ -219,7 +219,7 @@ class ZoomableActivity : BaseActivity() { onSwipe() } } - binding.zoomProgressBar?.let { + binding.zoomProgressBar.let { it.visibility = if (result.status is CallbackStatus.FETCHING) View.VISIBLE else View.GONE } } @@ -234,7 +234,7 @@ class ZoomableActivity : BaseActivity() { sharedPreferences.getBoolean(ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) if (!images.isNullOrEmpty()) { - binding.zoomable!!.setOnTouchListener( + binding.zoomable.setOnTouchListener( object : OnSwipeTouchListener(this) { // Swipe left to view next image in the folder. (if available) override fun onSwipeLeft() { @@ -271,7 +271,7 @@ class ZoomableActivity : BaseActivity() { * Handles down swipe action */ private fun onDownSwiped() { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -341,7 +341,7 @@ class ZoomableActivity : BaseActivity() { * Handles up swipe action */ private fun onUpSwiped() { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -414,7 +414,7 @@ class ZoomableActivity : BaseActivity() { * Handles right swipe action */ private fun onRightSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -451,7 +451,7 @@ class ZoomableActivity : BaseActivity() { * Handles left swipe action */ private fun onLeftSwiped(showAlreadyActionedImages: Boolean) { - if (binding.zoomable?.zoomableController?.isIdentity == false) { + if (binding.zoomable.zoomableController?.isIdentity == false) { return } @@ -646,7 +646,7 @@ class ZoomableActivity : BaseActivity() { .setProgressBarImage(ProgressBarDrawable()) .setProgressBarImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) .build() - with(binding.zoomable!!) { + with(binding.zoomable) { setHierarchy(hierarchy) setAllowTouchInterceptionWhileZoomed(true) setIsLongpressEnabled(false) @@ -658,10 +658,10 @@ class ZoomableActivity : BaseActivity() { .setUri(imageUri) .setControllerListener(loadingListener) .build() - binding.zoomable!!.controller = controller + binding.zoomable.controller = controller if (photoBackgroundColor != null) { - binding.zoomable!!.setBackgroundColor(photoBackgroundColor!!) + binding.zoomable.setBackgroundColor(photoBackgroundColor!!) } if (!images.isNullOrEmpty()) { diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java index 5d480f4f73..b5f760c9f7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyFilterSearchRecyclerViewAdapter.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import fr.free.nrw.commons.R; +import java.util.Locale; public class NearbyFilterSearchRecyclerViewAdapter extends RecyclerView.Adapter @@ -121,11 +122,11 @@ protected FilterResults performFiltering(CharSequence constraint) { results.count = labels.size(); results.values = labels; } else { - constraint = constraint.toString().toLowerCase(); + constraint = constraint.toString().toLowerCase(Locale.ROOT); for (Label label : labels) { String data = label.toString(); - if (data.toLowerCase().startsWith(constraint.toString())) { + if (data.toLowerCase(Locale.ROOT).startsWith(constraint.toString())) { filteredArrayList.add(Label.fromText(label.getText())); } } 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 d238296d13..299ac4b6e1 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 @@ -87,7 +87,7 @@ class WikidataFeedback : BaseActivity() { lat, lng, ) - } as Callable>, + }, ).subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe({ aBoolean: Boolean? -> diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index 9acf5b595f..c6d09fdc66 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -32,6 +32,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import javax.inject.Inject; /** @@ -139,14 +140,14 @@ private void setTabs() { leaderboardFragment.setArguments(leaderBoardBundle); fragmentList.add(leaderboardFragment); - titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase()); + titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase(Locale.ROOT)); contributionsFragment = new ContributionsFragment(); Bundle contributionsListBundle = new Bundle(); contributionsListBundle.putString(KEY_USERNAME, userName); contributionsFragment.setArguments(contributionsListBundle); fragmentList.add(contributionsFragment); - titleList.add(getString(R.string.contributions_fragment).toUpperCase()); + titleList.add(getString(R.string.contributions_fragment).toUpperCase(Locale.ROOT)); viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java index 46ea631fb1..f44b7eb6d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java @@ -27,6 +27,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.Locale; import java.util.Objects; import javax.inject.Inject; import org.apache.commons.lang3.StringUtils; @@ -361,7 +362,7 @@ private void inflateAchievements(Achievements achievements) { + levelInfo.getMaxUniqueImages()); binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); - String levelUpInfoString = getString(R.string.level).toUpperCase(); + String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); levelUpInfoString += " " + levelInfo.getLevelNumber(); binding.achievementLevel.setText(levelUpInfoString); binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java index c4a4bf518e..cbb8c8a1cc 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.recentlanguages; +import android.annotation.SuppressLint; import android.content.ContentProviderClient; import android.content.ContentValues; import android.database.Cursor; @@ -117,6 +118,7 @@ public boolean findRecentLanguage(final String languageCode) { * @return Language object */ @NonNull + @SuppressLint("Range") Language fromCursor(final Cursor cursor) { // Hardcoding column positions! final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java index 5eb758ada8..40d743a19a 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java @@ -25,6 +25,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.CompositeDisposable; import io.reactivex.schedulers.Schedulers; +import java.util.Locale; import javax.inject.Inject; public class ReviewActivity extends BaseActivity { @@ -241,7 +242,7 @@ public void onDestroy() { public void showSkipImageInfo(){ DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.skip_image).toUpperCase(), + getString(R.string.skip_image).toUpperCase(Locale.ROOT), getString(R.string.skip_image_explanation), getString(android.R.string.ok), "", diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index 876fb3cd3f..c0e5097c06 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -63,7 +63,7 @@ class FailedUploadsFragment : } if (StringUtils.isEmpty(userName)) { - userName = sessionManager!!.getUserName() + userName = sessionManager.getUserName() } } @@ -96,8 +96,8 @@ class FailedUploadsFragment : fun initRecyclerView() { binding.failedUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.failedUploadsRecyclerView.adapter = adapter - pendingUploadsPresenter!!.getFailedContributions() - pendingUploadsPresenter!!.failedContributionList.observe( + pendingUploadsPresenter.getFailedContributions() + pendingUploadsPresenter.failedContributionList.observe( viewLifecycleOwner, ) { list: PagedList -> adapter.submitList(list) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index b45e4b57da..8a8fa35b32 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -19,6 +19,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.Locale; import timber.log.Timber; public class FileUtils { @@ -139,7 +140,7 @@ public static String getMimeType(Context context, Uri uri) { String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri .toString()); mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension( - fileExtension.toLowerCase()); + fileExtension.toLowerCase(Locale.getDefault())); } return mimeType; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt index 4d79bc88e8..4442a64eaa 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsFragment.kt @@ -74,8 +74,8 @@ class PendingUploadsFragment : fun initRecyclerView() { binding.pendingUploadsRecyclerView.setLayoutManager(LinearLayoutManager(this.context)) binding.pendingUploadsRecyclerView.adapter = adapter - pendingUploadsPresenter!!.setup() - pendingUploadsPresenter!!.totalContributionList.observe( + pendingUploadsPresenter.setup() + pendingUploadsPresenter.totalContributionList.observe( viewLifecycleOwner, ) { list: PagedList -> contributionsSize = list.size diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java index 8503b1d05a..dd264655f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/UploadCategoriesFragment.java @@ -372,7 +372,7 @@ public void onResume() { return false; }); - Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + requireView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -387,7 +387,7 @@ public void onResume() { }); Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .hide(); if (getParentFragment().getParentFragment().getParentFragment() @@ -407,7 +407,7 @@ public void onStop() { super.onStop(); if (media != null) { Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .show(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java index bd52a8d351..9000e513d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsFragment.java @@ -398,7 +398,7 @@ public void onResume() { return false; }); - Objects.requireNonNull(getView()).setFocusableInTouchMode(true); + requireView().setFocusableInTouchMode(true); getView().requestFocus(); getView().setOnKeyListener((v, keyCode, event) -> { if (event.getAction() == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) { @@ -411,7 +411,7 @@ public void onResume() { }); Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .hide(); if (getParentFragment().getParentFragment().getParentFragment() @@ -431,7 +431,7 @@ public void onStop() { super.onStop(); if (media != null) { Objects.requireNonNull( - ((AppCompatActivity) Objects.requireNonNull(getActivity())).getSupportActionBar()) + ((AppCompatActivity) requireActivity()).getSupportActionBar()) .show(); } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java index 2c4c2ecd3d..5581cfeb1e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java @@ -825,7 +825,7 @@ private boolean listContainsEmptyDetails(List uploadMediaDeta @Override public void displayAddLocationDialog(final Runnable onSkipClicked) { isMissingLocationDialog = true; - DialogUtil.showAlertDialog(Objects.requireNonNull(getActivity()), + DialogUtil.showAlertDialog(requireActivity(), getString(R.string.no_location_found_title), getString(R.string.no_location_found_message), getString(R.string.add_location), diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java index 7152d4d8fe..cd533401b9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaPresenter.java @@ -129,9 +129,9 @@ public void receiveImage(final UploadableFile uploadableFile, final Place place, if (place.location != null) { final String countryCode = reverseGeoCode(place.location); if (countryCode != null && WLM_SUPPORTED_COUNTRIES - .contains(countryCode.toLowerCase())) { + .contains(countryCode.toLowerCase(Locale.ROOT))) { uploadItem.setWLMUpload(true); - uploadItem.setCountryCode(countryCode.toLowerCase()); + uploadItem.setCountryCode(countryCode.toLowerCase(Locale.ROOT)); } } } diff --git a/app/src/main/res/layout/activity_description_edit.xml b/app/src/main/res/layout/activity_description_edit.xml index ed50193a21..1a8d3b8cea 100644 --- a/app/src/main/res/layout/activity_description_edit.xml +++ b/app/src/main/res/layout/activity_description_edit.xml @@ -36,11 +36,11 @@ android:layout_height="wrap_content" android:layout_marginStart="16dp" android:contentDescription="@string/exit_location_picker" - android:tint="@color/white" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintTop_toTopOf="parent" - app:srcCompat="@drawable/ic_arrow_back_white" /> + app:srcCompat="@drawable/ic_arrow_back_white" + app:tint="@color/white" /> @@ -69,7 +69,7 @@ android:id="@+id/btn_edit_submit" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:text="@string/submit" android:textColor="@android:color/white" /> diff --git a/app/src/main/res/layout/bottom_sheet_details_explore.xml b/app/src/main/res/layout/bottom_sheet_details_explore.xml index 1da5c7f3ee..6558c9afe9 100644 --- a/app/src/main/res/layout/bottom_sheet_details_explore.xml +++ b/app/src/main/res/layout/bottom_sheet_details_explore.xml @@ -31,7 +31,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="16sp" - android:layout_marginRight="50dp" + android:layout_marginEnd="50dp" android:maxLines="2" android:ellipsize="end" /> @@ -58,6 +58,7 @@ android:layout_width="@dimen/dimen_0" android:layout_height="wrap_content" android:layout_weight="1" + android:focusable="true" android:padding="@dimen/standard_gap" android:clickable="true" android:background="@drawable/button_background_selector" @@ -69,8 +70,7 @@ android:layout_gravity="center_horizontal" android:duplicateParentState="true" app:srcCompat="@drawable/ic_directions_black_24dp" - android:tint="?attr/rowButtonColor" - /> + app:tint="?attr/rowButtonColor" /> diff --git a/app/src/main/res/layout/bottom_sheet_item_layout.xml b/app/src/main/res/layout/bottom_sheet_item_layout.xml index 4f4c2c8546..c569e523a9 100644 --- a/app/src/main/res/layout/bottom_sheet_item_layout.xml +++ b/app/src/main/res/layout/bottom_sheet_item_layout.xml @@ -1,11 +1,13 @@ @@ -14,7 +16,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:tint="?attr/rowButtonColor" /> + app:tint="?attr/rowButtonColor" /> @@ -36,7 +35,6 @@ style="?android:textAppearanceLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_horizontal" android:text="@string/level" @@ -48,13 +46,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/activity_margin_vertical" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/black" - android:layout_marginVertical="@dimen/activity_margin_vertical" /> + android:layout_marginVertical="@dimen/activity_margin_vertical" + app:tint="@color/black" /> + android:layout_marginStart="@dimen/activity_margin_horizontal" + app:tint="@color/primaryLightColor" /> @@ -189,7 +182,6 @@ style="?android:textAppearanceMedium" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:id="@+id/images_reverted_text" android:layout_marginStart="@dimen/activity_margin_horizontal" android:text="@string/image_reverts" /> @@ -200,24 +192,19 @@ android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_toRightOf="@+id/images_reverted_text" - android:layout_toEndOf="@+id/images_reverted_text" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" android:layout_marginLeft="@dimen/activity_margin_horizontal" - android:layout_marginStart="@dimen/activity_margin_horizontal"/> + android:layout_marginStart="@dimen/activity_margin_horizontal" app:tint="@color/primaryLightColor" /> - @@ -278,7 +265,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/images_used_by_wiki_text" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/achievements_activity_margin_vertical" android:text="@string/images_used_by_wiki" /> @@ -289,12 +275,10 @@ android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" - android:layout_toRightOf="@+id/images_used_by_wiki_text" - android:layout_toEndOf="@+id/images_used_by_wiki_text" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" android:layout_marginLeft="@dimen/activity_margin_horizontal" - android:layout_marginStart="@dimen/activity_margin_horizontal"/> + android:layout_marginStart="@dimen/activity_margin_horizontal" + app:tint="@color/primaryLightColor" /> @@ -353,7 +337,6 @@ android:layout_height="wrap_content" android:text="@string/statistics" style="?android:textAppearanceLarge" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_vertical" android:textAllCaps="true"/> @@ -373,9 +356,7 @@ android:id="@+id/images_nearby_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/wikidata_edits" - android:layout_toLeftOf="@+id/wikidata_edits" android:orientation="horizontal" android:gravity="center_vertical"> @@ -407,14 +388,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/images_nearby_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" android:layout_gravity="top" app:layout_constraintLeft_toRightOf="@id/images_nearby_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -423,16 +403,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/wikidata_edits" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -451,9 +429,7 @@ android:id="@+id/images_featured_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/image_featured" - android:layout_toLeftOf="@+id/image_featured" android:orientation="horizontal" android:gravity="center_vertical"> @@ -486,14 +462,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/images_featured_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/images_featured_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -501,16 +476,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/image_featured" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -529,9 +502,7 @@ android:id="@+id/quality_images_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/quality_images" - android:layout_toLeftOf="@+id/quality_images" android:orientation="horizontal" android:gravity="center_vertical"> @@ -564,14 +535,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/quality_images_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/quality_images_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -579,7 +549,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" @@ -587,9 +556,8 @@ tools:text="2" android:text="0" android:id="@+id/quality_images" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> @@ -608,9 +576,7 @@ android:id="@+id/thanks_received_info" android:layout_centerVertical="true" android:layout_alignParentStart="true" - android:layout_alignParentLeft="true" android:layout_toStartOf="@+id/thanks_received" - android:layout_toLeftOf="@+id/thanks_received" android:orientation="horizontal" android:gravity="center_vertical"> @@ -643,14 +609,13 @@ android:layout_height="@dimen/medium_height" android:id="@+id/thanks_received_info_icon" android:layout_marginTop="@dimen/activity_margin_horizontal" - android:layout_marginRight="@dimen/activity_margin_horizontal" android:layout_marginEnd="@dimen/activity_margin_horizontal" app:layout_constraintLeft_toRightOf="@id/thanks_received_data" app:layout_constraintTop_toTopOf="parent" app:layout_constraintRight_toRightOf="parent" android:layout_gravity="top" app:srcCompat="@drawable/ic_info_outline_24dp" - android:tint="@color/primaryLightColor" /> + app:tint="@color/primaryLightColor" /> @@ -658,16 +623,14 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="?android:textAppearanceMedium" - android:layout_alignParentRight="true" android:layout_alignParentEnd="true" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginStart="@dimen/activity_margin_horizontal" - android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_centerVertical="true" tools:text="2" android:id="@+id/thanks_received" android:layout_marginEnd="@dimen/half_standard_height" - android:layout_marginRight="@dimen/half_standard_height" /> + /> diff --git a/app/src/main/res/layout/item_place.xml b/app/src/main/res/layout/item_place.xml index 82e4310631..9854fb58d4 100644 --- a/app/src/main/res/layout/item_place.xml +++ b/app/src/main/res/layout/item_place.xml @@ -11,8 +11,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/standard_gap" - android:tint="?attr/rowButtonColor" - app:srcCompat="@drawable/ic_round_star_border_24px" /> + app:srcCompat="@drawable/ic_round_star_border_24px" + app:tint="?attr/rowButtonColor" /> @@ -54,11 +52,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignTop="@id/distance" - android:layout_marginLeft="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" android:layout_toEndOf="@id/icon" - android:layout_toLeftOf="@id/distance" - android:layout_toRightOf="@id/icon" android:layout_toStartOf="@id/distance" android:ellipsize="end" android:maxLines="2" @@ -71,8 +66,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignEnd="@id/distance" - android:layout_alignLeft="@id/tvName" - android:layout_alignRight="@id/distance" android:layout_alignStart="@id/tvName" android:layout_below="@id/tvName" android:layout_marginBottom="@dimen/standard_gap" diff --git a/app/src/main/res/layout/layout_campagin.xml b/app/src/main/res/layout/layout_campagin.xml index 775a6a4ece..2a2891e84e 100644 --- a/app/src/main/res/layout/layout_campagin.xml +++ b/app/src/main/res/layout/layout_campagin.xml @@ -19,17 +19,14 @@ android:id="@+id/iv_campaign" android:layout_width="@dimen/dimen_40" android:layout_height="@dimen/dimen_40" - android:layout_marginLeft="@dimen/standard_gap" android:layout_marginStart="@dimen/standard_gap" android:scaleType="centerCrop" app:srcCompat="@drawable/ic_campaign" - android:tint="?attr/card_item_color" - /> + app:tint="?attr/card_item_color" /> @@ -37,15 +34,13 @@ + android:layout_marginStart="@dimen/standard_gap" + android:layout_marginEnd="@dimen/tiny_margin"> + android:visibility="visible" + app:tint="?attr/contributionsListTextSecondary" /> diff --git a/app/src/main/res/layout/nearby_card_view.xml b/app/src/main/res/layout/nearby_card_view.xml index 7161a09363..bbd43249e6 100644 --- a/app/src/main/res/layout/nearby_card_view.xml +++ b/app/src/main/res/layout/nearby_card_view.xml @@ -14,7 +14,6 @@ style="@style/Widget.AppCompat.Button.Borderless" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerInParent="true" android:layout_marginLeft="@dimen/activity_margin_horizontal" android:layout_marginTop="@dimen/activity_margin_horizontal" android:layout_marginRight="@dimen/activity_margin_horizontal" @@ -30,34 +29,28 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/content_layout" - android:layout_centerInParent="true" android:orientation="horizontal" > + android:id="@+id/progressBar" /> + app:tint="?attr/card_item_color" /> @@ -24,8 +25,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_photo_camera_white_24dp" /> + app:srcCompat="@drawable/ic_photo_camera_white_24dp" + app:tint="?attr/bookmarkButtonColor" /> @@ -53,8 +55,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:duplicateParentState="true" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_photo_white_24dp" /> + app:srcCompat="@drawable/ic_photo_white_24dp" + app:tint="?attr/bookmarkButtonColor" /> + android:duplicateParentState="true" + app:tint="?attr/bookmarkButtonColor" /> @@ -110,8 +114,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:duplicateParentState="true" - android:tint="?attr/bookmarkButtonColor" - app:srcCompat="@drawable/ic_overflow" /> + app:srcCompat="@drawable/ic_overflow" + app:tint="?attr/bookmarkButtonColor" /> + app:srcCompat="@drawable/ic_arrow_back_white" + app:tint="@color/white" /> \ No newline at end of file diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 9ff1b19b4c..22f382f576 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭаларҭа Иҟаҵатәуп арегистрациа Асистемахь аҭаларҭа - Шәааԥшы ԥыҭрак... + Шәааԥшы ԥыҭрак… Аҭалара қәҿиарала имҩаԥысит! Асистемахь аҭалараан агха! Афаил ԥшаам. Даҽа фаилк шәахәаԥш. @@ -64,7 +64,7 @@ Ари шәара еилышәкаама? Ааи! Акатегориақәа - Аҭагалара... + Аҭагалара… Акагь алхӡам Иҟам ахҳәаа Идырым алицензиа diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 1da8b3101f..57ba77cc93 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -22,7 +22,6 @@ %1$d lêers aan die uploaden - \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -148,7 +147,7 @@ Ja! <u>Meer inligting</u> Kategorieë - Laai ... + Laai … Niks gekies nie Geen beskrywing Geen bespreking nie diff --git a/app/src/main/res/values-anp/strings.xml b/app/src/main/res/values-anp/strings.xml index 70a01949f2..e4029af9b0 100644 --- a/app/src/main/res/values-anp/strings.xml +++ b/app/src/main/res/values-anp/strings.xml @@ -27,14 +27,14 @@ पासवर्ड भूलाय गेलौ की? साइन अप करौ प्रवेश होय रहलौ छौं - कृपया प्रतीक्षा करौ... - कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ… प्रवेश विफल अपलोड आरंभ! हाल केरौ अपलोड कतारबद्ध विफल - अपलोड होय रहलौ छौं... + अपलोड होय रहलौ छौं… ठामे मँ हमरौ अपलोड साझा करौ @@ -68,7 +68,7 @@ हाँव! बेसी जानकारी श्रेणी सिनी - लोड होय रहलौ छौं... + लोड होय रहलौ छौं… कुछु चयनित नाय कोय शीर्षक नाय कोय विवरण नाय @@ -173,7 +173,7 @@ पूर्ण होलौं अगलका छवि हाँव, केन्हअ नाय - कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ… प्रतिलिपि बनैलौ गेलै! लेखक स्थान diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 46ffcb7f56..b0fda69907 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار... + يرجى الانتظار… نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -530,7 +530,7 @@ عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار... + الرجاء الانتظار… الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +552,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر... + مشاركة التطبيق عبر… معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -695,7 +695,7 @@ وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل ... + جاري استئناف التحميل … جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 960b55bdad..63aa12b964 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -27,7 +27,7 @@ পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক লগইন হৈ আছে - অনুগ্ৰহ কৰি অপেক্ষা কৰক... + অনুগ্ৰহ কৰি অপেক্ষা কৰক… লগইন সফল হ\'ল! লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। @@ -74,7 +74,7 @@ <u>গোপনিয়তা নীতি</u> প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই - প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত… আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index df61ed0610..34212aebbc 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -74,7 +74,7 @@ Aniciando sesión Espera… Actualizando pies y descripciones - Porfavor espera... + Porfavor espera… ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. @@ -480,7 +480,7 @@ Númberos de serie Software Xubi semeyes a Wikimedia Commons direutamente dende\'l to móvil. Descarga yá la app de Commons: %1$s - Compartir l\'aplicación per... + Compartir l\'aplicación per… Información de la imaxe Nun s\'alcontró nenguna categoría Nun s\'alcontraron retratos diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 1edbe43fce..d2ea468ad7 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -104,7 +104,7 @@ CC BY 3.0 Əlavə məlumat Kateqoriyalar - Yüklənir... + Yüklənir… Heç biri seçilməmişdir Naməlum lisenziya Yenilə diff --git a/app/src/main/res/values-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 4fa660ef88..8e77643235 100644 --- a/app/src/main/res/values-b+roa+tara/strings.xml +++ b/app/src/main/res/values-b+roa+tara/strings.xml @@ -40,8 +40,8 @@ Tràse Passuord scurdate? Reggistrate - Stoche a tràse... - Aspitte... + Stoche a tràse… + Aspitte… E\' trasute! Non g\'è trasute! File non acchiate. Pruève \'n\'otre file. @@ -121,7 +121,7 @@ Permesse richieste Non ge tìne notifeche non lette Errore assute mendre ca ste pigghiave le immaggine - Aspitte... + Aspitte… Zumbe ste immaggine Autore Lènghe d\'a descrizione predefinite diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index cd1cb09e8f..b8b602d0dc 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -5,7 +5,7 @@ * Milicevic01 * Zoranzoki21 --> - + Fejsbuk stranica Ostave Izvorni kod na Github-u Logo Ostave @@ -26,32 +26,39 @@ Slika dana %1$d datoteka se otprema + %1$d datoteke se otpremaju %1$d datoteke se otpremaju %1$d otpremanje + %1$d otpremanja %1$d otpremanja Pokretanje otpremanja Procesuiranje %d otpremanje + Procesuiranje %d otpremanja Procesuiranje %d otpremanja %d otpremanje + %d otpremanja %d otpremanja Slika će se voditi pod licencom %1$s + Slike će se voditi pod licencom %1$s Slike će se voditi pod licencom %1$s %1$d otpremanje + %1$d otpremanja %1$d otpremanja - Primanje deljenog sadržaja... Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja - Primanje deljenog sadržaja... Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja Istraga Izgled @@ -486,7 +493,7 @@ Pristup lokaciji medija je odbijen Možda nećemo moći da automatski pribavimo podatke o lokaciji iz slika koje otpremite. Dodajte odgovarajuću lokaciju za svaku sliku pre objavljivanja Otpremi fotografije na Vikimedijinu Ostavu direktno sa svog telefona. Preuzmi aplikaciju Ostave sada: %1$s - Podeli aplikaciju preko... + Podeli aplikaciju preko… Informacije o slici Nisu pronađene kategorije Otkazano otpremanje @@ -511,12 +518,13 @@ Uspešno Kategorija %1$s je dodata. + Kategorije %1$s su dodate. Kategorije %1$s su dodate. Nije moguće dodati kategorije. Ažuriraj kategoriju Uredi prikaze - Pokušavanje promena koordinata... + Pokušavanje promena koordinata… Ažuriranje koordinata Ažuriranje opisa Ažuriranje natpisa @@ -698,6 +706,7 @@ Nije moguće podeliti ovu stavku %d slika je odabrana + %d slika je odabrano %d slika je odabrano diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 0fc68329f1..4c33b396fe 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -61,9 +61,9 @@ Серһүҙҙе оноттоғоҙмо? Теркәлеү Системаға инеү - Зинһар, көтөгөҙ... + Зинһар, көтөгөҙ… Аңлатмалар һәм тасуирламалар яңыртыла - Зинһар, көтөгөҙ... + Зинһар, көтөгөҙ… Системаға инеү уңышлы! Системаға инеү уңышһыҙ! Файл табылманы. Башҡа файлды эҙләп ҡарағыҙ. @@ -131,7 +131,7 @@ Фекереңде ебәр (эл.почта аша) Почта клиенты асыҡланмаған Яңыраҡ ҡулланылған категориялар - Тәүге синхронлаштырыуҙы көтөү... + Тәүге синхронлаштырыуҙы көтөү… Әлегә бер фото ла йөкләмәгәнһегеҙ Ҡабатларға Кире алыу @@ -171,7 +171,7 @@ Эйе! Ентеклерәк Категориялар - Йөкләнә башланы... + Йөкләнә башланы… Бер ни ҙә һайланмаған Тасуирламаһы юҡ Фекер алышыу юҡ diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index b24bc00221..1273eaf0d6 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -61,7 +61,7 @@ Lali kruna Sandi? Daftar Ngeranjingin log - Jantos dumun... + Jantos dumun… Nganyarin sesirah miwah pidarta Jantos dumun… Mahasil manjing log! @@ -303,7 +303,7 @@ Nomor seri Piranti lunak Unggah foto nuju Wikimédia Commons langsung saking télépon ragané. Unduh aplikasi Commons mangkin: %1$s - Wedar aplikasi saking... + Wedar aplikasi saking… Pidarta Gambar Pangunggahan Kawangdé %1$s kaunggah olih: %2$s @@ -340,7 +340,7 @@ Kaanggén Paringkat Titiang Kualitas Gambar - Ngalanturang unggahan... + Ngalanturang unggahan… Ngarérénang unggahan… Wangdé Unggah Lisénsi Média diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 6ee9315423..cb19d6e39f 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -304,7 +304,7 @@ Преглеждане на прочетени Преглеждане на непрочетени Възникна грешка при избирането на изображенията - Моля, изчакайте... + Моля, изчакайте… напълно размазано Наблизо Прочетете повече diff --git a/app/src/main/res/values-blk/strings.xml b/app/src/main/res/values-blk/strings.xml index 2cf4aba5bd..51a8ec1ba8 100644 --- a/app/src/main/res/values-blk/strings.xml +++ b/app/src/main/res/values-blk/strings.xml @@ -38,7 +38,7 @@ အွောန်ႏဖေင်ꩻထိုꩻ ငဝ်းဗိဉ်ႏပလို့ꩻနဲ့? ဒင်ႏမတ်ပိုင်တိဉ် အဝ်ႏနွို့အကောက်ကျာꩻ - အိုင်ပွေားဆောင်းတဆင်ႏသြ... + အိုင်ပွေားဆောင်းတဆင်ႏသြ… နွို့အကောက်အောင်ႏလဲဉ်း! နွို့အကောက်အောင်ႏတဝ်း! မော့ꩻတဝ်းဖုဲင်၊ စံꩻထွားစံꩻသွော့ ဖုဲင်အလင်တဗာႏသြ။ @@ -97,7 +97,7 @@ မွေး! ထဲင်းယင်း သꩻတင်ꩻအချက်လက် ကဏ္ဍဖုံႏ - အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ... + အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ… လွိုက်ခါꩻတဝ်းမုဲင်ꩻမုဲင်ꩻ ပုင်ႏလိတ်အဝ်ႏတဝ်း အွောန်ႏနယ်ချက်အဝ်ႏတဝ်း diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 2d156c1999..51502c2649 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -393,7 +393,7 @@ কোনও চিত্র ব্যবহৃত হয়নি পঠিতগুলি দেখান অপঠিতগুলি দেখান - অনুগ্রহ করে অপেক্ষা করুন... + অনুগ্রহ করে অপেক্ষা করুন… অনুলিপি করা হয়েছে এই চিত্র এড়িয়ে যান প্রণেতা diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 1c7d09617a..9537c45e62 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -40,6 +40,9 @@ %1$d bellgargadenn loc\'het + %1$d bellgargadenn loc\'het + %1$d bellgargadennoù loc\'het + %1$d bellgargadennoù loc\'het %1$d pellgargadennoù loc\'het @@ -51,6 +54,9 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ + gant an aotre-implijout %1$s e vo an div skeudenn-mañ + gant an aotre-implijout %1$s e vo meur a skeudenn-mañ + gant an aotre-implijout %1$s e vo kalz a skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ Ergerzhout diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 91860b1e12..d178ff507a 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -10,19 +10,22 @@ Logo Commonsa postavlja se %1$d datoteka + postavlja se %1$d datoteke postavlja se %1$d datoteka - \@string/contributions_subtitle_zero postavljena %1$d datoteka + postavljena %1$d datoteke postavljenih datoteka: %1$d Započinjem postavljanje %1$d datoteke + Započinjem postavljanje %1$d datoteke Započinjem postavljanje %1$d datoteka/-e %1$d postavljanje + %1$d postavljanja %1$d postavljanja Slika će se voditi pod licencom %1$s diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 0c15e58c34..51330cb9d2 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,27 +20,33 @@ Imatge del dia s\'està carregant %1$d fitxer + S\'estan carregant de %1$d fitxers s\'estan carregant %1$d fitxers (%1$d) + (%1$d) (%1$d) S\'inicien les càrregues S\'està processant %1$d càrrega + S\'estan processant %1$d càrregues S\'estan processant %1$d càrregues %d càrrega + $d càrregues %d càrregues Aquesta imatge quedarà sota llicència %1$s + Aquestes imatges quedaran sota llicència %1$s Aquestes imatges quedaran sota llicència %1$s %1$d pujada + %1$d pujades %1$d pujades Explora @@ -392,7 +398,7 @@ Model de lent Números de sèrie Programari - Comparteix l\'aplicació a través de... + Comparteix l\'aplicació a través de… Informació de la imatge No s’ha trobat cap categoria No s\'han trobat representacions diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index e13e8c040d..e25b83e259 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -64,7 +64,7 @@ Викиларма Параметраш Викиларма чуйаккха - ДӀадоьдуш ду чуйаккхар... + ДӀадоьдуш ду чуйаккхар… Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо @@ -146,7 +146,7 @@ ЦӀе: Сиднейн операн театр ХӀаъ! Категореш - Чуйолуш... + Чуйолуш… ХӀума хаьржина йац Куьг доцуш Хаамаш бац @@ -297,7 +297,7 @@ Серийн лоьмар Программан кхачам Файл йолу меттиган тӀекхача бакъо ца ло - Йекъа программа, гӀоьнца... + Йекъа программа, гӀоьнца… Суьртан информаци Цхьа а категори ца карийна. Цхьа а хаам ца карийна. @@ -362,7 +362,7 @@ ДӀайаьккхина закладки йукъара Цхьа хӀума галдаьлла. Фонан сурт хӀотто аьтто ца баьлла Фонан сурт санна хӀоттайе - Фонан сурт дӀахӀоттош ду... + Фонан сурт дӀахӀоттош ду… Системин нисдаран гӀирс Бодане Сирла diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4d49ee6327..fb4ee05ef3 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,31 +32,44 @@ Obrázek dne %1$d soubor se nahrává + %1$d soubory se nahrávají + %1$d souborů se nahrává %1$d souborů se nahrává - \@string/contributions_subtitle_zero (%1$d) + (%1$d) + (%1$d) (%1$d) Spouští se nahrávání %1$d souboru + Spouští se nahrávání %1$d souborů + Spouští se nahrávání %1$d souborů Spouští se nahrávání %1$d souborů %1$d nahrávání + %1$d nahrávání + %1$d nahrávání %1$d nahrávání Tento obrázek bude zveřejněn pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s %1$d nahrání + %1$d nahrávání + %1$d nahrávání %1$d nahrání Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení + Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení + Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Objevit diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 623a48c8c2..7cdfb13825 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -29,7 +29,7 @@ Wlogùjë mie Wregistrëjë sã Logòwanié - Proszã żdac... + Proszã żdac… Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. @@ -78,7 +78,7 @@ Sélôj òpinijã (przez e-mail) Felënk wjinstalowónegò e-mailowégò klienta Slédno ùżëwóne kategòrëje - Żdanié na pierszą synchronizacëjã... + Żdanié na pierszą synchronizacëjã… Nie môsz jesz wladowónych òdjimków Próbùjë znowa Òprzestóń @@ -99,7 +99,7 @@ Przëmiôr wladënka: Jo! Kategòrëje - Wladënk... + Wladënk… Felënk nacéchòwaniô Felënk òpisënka Nieznónô licencëja diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 8c4b4a652a..50df9b6d8c 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -21,25 +21,44 @@ Popeth Llun y Dydd + %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho + %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho \@string/contributions_subtitle_zero (%1$d) + (%1$d) + (%1$d) + (%1$d) (%1$d) Cychwyn Uwchlwytho + Dechrau %1$d uwchlwythiad Cychwyn %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad + Dechrau %1$d uwchlwythiad Cychwyn uwchlwytho %1$d ffeil + %1$d uwchlwythiad %1$d uwchlwythiad + %1$d uwchlwythiad + %1$d uwchlwythiad + %1$d uwchlwythiad %1$d uwchlwythiad + Ni chaiff unrhyw ddelweddau eu trwyddedu dan %1$s Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s + Caiff y delweddau hyn eu trwyddedu dan %1$s Caiff y delweddau hyn eu trwyddedu dan %1$s Archwilio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 3b6822c47b..80a82afb54 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,7 @@ Adgang til medieplacering nægtet Vi kan muligvis ikke automatisk indhente placeringsdata fra billeder, du uploader. Tilføj den passende placering for hvert billede, før du indsender Upload billeder til Wikimedia Commons direkte fra din telefon. Download Commons-appen nu: %1$s - Del app via... + Del app via… Billedoplysninger Ingen kategorier blev fundet Ingen afbildninger fundet @@ -642,9 +642,9 @@ Begrænset forbindelsestilstand Kvalitetsbilleder Kvalitetsbilleder er tegninger eller fotografier, der opfylder visse kvalitetsstandarder (som for det meste er af teknisk karakter) og er værdifulde for Wikimedia-projekter - Genoptager upload... - Sætter upload på pause... - Annullerer upload... + Genoptager upload… + Sætter upload på pause… + Annullerer upload… Annuller upload Du har aktiveret begrænset forbindelsestilstand. Alle uploads er sat på pause og genoptages, når du deaktiverer denne tilstand. Begrænset forbindelsestilstand aktiveret! @@ -784,7 +784,7 @@ Andet problem eller anden information (forklar venligst nedenfor). Din feedback bliver slået op på følgende wiki-side: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Er du sikker på, at du vil annullere alle uploads? - Annullerer alle uploads... + Annullerer alle uploads… Uploads Afventer Mislykkedes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a6471c1fe9..4043040214 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -516,7 +516,7 @@ Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten Bitte warten … - Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. + Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. Über Orte in der Nähe hochgeladene Bilder sind die Bilder, die von entdeckten Orten auf der Karte hochgeladen wurden. Diese Funktion erlaubt es Autoren, eine Dankeschön-Benachrichtigung an Benutzer zu senden, die nützliche Bearbeitungen durchgeführt haben – durch die Benutzung eines kleinen Dankeschön-Links in der Versionsgeschichte oder Unterschiedsseite. Auf Folgemedien kopieren @@ -611,7 +611,7 @@ zu den Lesezeichen hinzugefügt Etwas ist schiefgelaufen. Das Hintergrundbild konnte nicht eingestellt werden Als Hintergrundbild festlegen - Hintergrundbild wird festgelegt. Bitte warten... + Hintergrundbild wird festgelegt. Bitte warten… Systemeinstellung Dunkel Hell diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index 840b0198dc..ebb3cfbe4e 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -62,8 +62,8 @@ Parola, xo vira kerde? Qeyd be Kewno cı - Kerem kerên, bıpawên... - Kerem ke, bıpawe... + Kerem kerên, bıpawên… + Kerem ke, bıpawe… Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. @@ -93,7 +93,7 @@ Şınasnayış Bınnuşte Xırabiya kewten-network xeta - Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. + Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2–3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Kodê kamiya raştkerdışi dıfaktorın gani cı kewê. Nidekeweya de @@ -298,7 +298,7 @@ Pêhesnayışê toyê wendışi çıniyê Wendışi bıvêne Nêwendeyan bıvêne - Kerem kerên, bıpawên... + Kerem kerên, bıpawên… Nê resımi raviyarnê Nuştekar Heqa telifi diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e3675ef0a3..e4b597fb12 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Σύνδεση Ξεχάσατε τον κωδικό πρόσβασης σας; Εγγραφή - Γίνεται σύνδεση... + Γίνεται σύνδεση… Παρακαλούμε αναμείνετε… Ενημέρωση λεζάντων και περιγραφών Παρακαλούμε αναμείνετε… @@ -204,7 +204,7 @@ Ναι! Περισσότερες πληροφορίες Κατηγορίες - Φόρτωση σε εξέλιξη... + Φόρτωση σε εξέλιξη… Καμία επιλεγμένη Χωρίς λεζάντα Χωρίς περιγραφή @@ -521,7 +521,7 @@ Δεν επιτρέπεται η πρόσβαση στην τοποθεσία πολυμέσων Ενδέχεται να μην μπορούμε να λάβουμε αυτόματα δεδομένα τοποθεσίας από φωτογραφίες που ανεβάζετε. Προσθέστε την κατάλληλη τοποθεσία για κάθε εικόνα πριν την υποβολή Ανεβάστε φωτογραφίες στα Wikimedia Commons απευθείας από το τηλέφωνό σας. Κάντε λήψη της εφαρμογής Commons τώρα: %1$s - Κοινή χρήση εφαρμογής μέσω... + Κοινή χρήση εφαρμογής μέσω… Πληροφορίες Εικόνας Δεν βρέθηκαν Κατηγορίες Δεν βρέθηκαν απεικονίσεις @@ -798,7 +798,7 @@ Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω). Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a> Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις; - Ακύρωση όλων των μεταφορτώσεων... + Ακύρωση όλων των μεταφορτώσεων… Μεταφορτώσεις Σε εκκρεμότητα Απέτυχε diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 69673afbed..323c823b25 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -78,7 +78,7 @@ Ĉu pasvorto forgesita? Registriĝi Ensalutado - Bonvolu atendi... + Bonvolu atendi… Ĝisdatiganta subtekstojn kaj priskribojn Bonvolu atendi… Ensalutado sukcesis @@ -150,7 +150,7 @@ Sendi viajn komentojn (per retpoŝto) Neniu retpoŝtilo instalita Laste uzitaj kategorioj - Atendas la unuan Sinkronigado... + Atendas la unuan Sinkronigado… Vi ankoraŭ ne alŝutis fotojn. Reprovi Nuligi @@ -190,7 +190,7 @@ Jes! <u>Ekscii pli</u> Kategorioj - Ŝargado... + Ŝargado… Neniu elektita Neniu substeksto Sen priskribo @@ -482,7 +482,7 @@ Vidu legitajn Vidi nelegitojn Eraro okazis dum elektado de bildoj - Bonvolu atendi... + Bonvolu atendi… Elstaraj bildoj estas tiuj bildoj far tre spertaj fotografistoj kaj ilustristoj, kiujn la komunumo de Vikimedia Komunejo elektis kiel iujn de la plej alta kvalito en la retejo. Bildoj Alŝutitaj per Apudaj lokoj estas bildoj alŝutitaj per trovado de lokoj sur la mapo. Tiu funkcio ebligas sendi Dankantan sciigon al farinto de utila redakto – per malgranda dankiga ligilo ĉe la paĝo de historio aŭ diferenco. @@ -504,7 +504,7 @@ Aliro al loko de plurmediaĵo malakceptita Ni eble ne povos aŭtomate akiri pri-lokajn datumojn de bildoj, kiujn vi alŝutas. Bonvolu aldoni la taŭgan lokon por ĉiu bildo antaŭ ol sendi Alŝutu fotojn al Vikimedia Komunejo rekte de via telefono. Elŝutu la Komunejan aplikaĵon nun: %1$s - Diskonigi aplikaĵon per... + Diskonigi aplikaĵon per… Informo pri Bildo Neniu Kategorio troviĝis Neniu bildo-priskribo trovita @@ -636,9 +636,9 @@ Modo por limigita konekto Kvalitaj Bildoj Kvalitaj bildoj estas diagramoj aŭ fotoj kiuj kontentigas certajn normojn pri kvalito (kiuj estas plejparte teknikaj) kaj estas valoraj por Vikimediaj projektoj. - Rekomencante alŝuton... - Paŭzante alŝuton... - Nuligante alŝuton... + Rekomencante alŝuton… + Paŭzante alŝuton… + Nuligante alŝuton… Ĉesigi alŝutadon Vi aktivigis Modon por limigita konekto. Ĉiuj alŝutoj estas paŭzitaj kaj rekomencos post kiam vi malŝaltos ĉi modon. Modo por limigita konekto estas aktivigita. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4e90f68641..2189534705 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,7 +51,7 @@ * Vivaelcelta * Wizardeck --> - + Página de Facebook de Commons Código fuente de Commons en GitHub Logo de Commons @@ -75,31 +75,38 @@ Foto del día Cargando %1$d archivo + Cargando %1$d archivos Cargando %1$d archivos (%1$d) + (%1$d) (%1$d) Comenzando las subidas Procesando %d carga + Procesando %d cargas Procesando %d cargas %d carga + %1 cargas %1 cargas Esta imagen se publicará bajo la licencia %1$s + Estas imágenes se publicarán bajo la licencia %1$s Estas imágenes se publicarán bajo la licencia %1$s %1$d Subida + %1$d Subidas %1$d Subidas Recepción de contenido compartido. El procesamiento de la imagen puede tardar cierto tiempo, dependiendo del tamaño de la imagen y de tu dispositivo + Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Explorar @@ -335,7 +342,7 @@ Omitir tutorial Internet no disponible Error al recuperar las notificaciones - Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. + Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. No se encontró ninguna notificación Traducir Idiomas @@ -477,7 +484,7 @@ Permitir Descartar Por favor, activa el acceso a la ubicación desde Configuración y vuelva a intentarlo. \n\nNota: Es posible que la subida no tenga datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. - La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. + La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. La aplicación no registrará la ubicación junto con las tomas debido a la falta del permiso de la ubicación. La aplicación no registrará la ubicación junto con las tomas porque el GPS está apagado Utilizar el selector de fotografías basado en documentos @@ -505,8 +512,8 @@ ¿Está correctamente categorizado? ¿Está dentro de los objetivos del proyecto? ¿Quieres agradecer al colaborador? - Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. - Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado + Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. + Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado Tu apreciación animara a %1$s ¡Oh, esto ni siquiera esta categorizado! Esta imagen esta dentro de %1$s categorías. @@ -524,7 +531,7 @@ Compartir registros usando Ver leídas Ver no leidas - Ocurrió un error mientras se elegían imagenes + Ocurrió un error mientras se elegían imágenes Un momento… Las imágenes destacadas son creaciones de talentosos fotógrafos e ilustradores que la comunidad de Wikimedia Commons ha reconocido como las de mayor calidad del sitio. Las imágenes subidas vía Lugares Cercanos son las imágenes que han sido subidas al descubrir lugares en el mapa. @@ -547,7 +554,7 @@ Acceso a la ubicación del archivo multimedia denegado Es posible que no podamos obtener automáticamente los datos de ubicación de las imágenes que suba. Añada la ubicación adecuada a cada imagen antes de enviarla Sube fotos a Wikimedia Commons directamente desde tu celular. Descarga la aplicación de Commons ahora: %1$s - Compartir la aplicación vía... + Compartir la aplicación vía… Información de la imagen No se encontró ninguna categoría No se encontraron representaciones @@ -574,6 +581,7 @@ Éxito Se añade %1$s categoría. + Se añaden %1$s categorías. Se añaden %1$s categorías. No se pudieron añadir las categorías. @@ -582,6 +590,7 @@ Editar las descripciones %1$s Se añade la descripción. + Descripción %1$s se añadieron. Descripción %1$s se añadieron. No se pueden añadir descripciones. @@ -599,7 +608,7 @@ Las coordenadas de la imagen no están actualizadas. No se puede obtener descripciones. Editar descripciones y leyendas - Compartir imagen via + Compartir imagen via Todavía no has hecho ninguna contribución. %s Aún no ha realizado ninguna contribución Cuenta creada @@ -624,7 +633,7 @@ añadido a marcadores Algo salió mal. No se pudo establecer el fondo de pantalla Colocar como fondo de pantalla - Estableciendo el fondo de pantalla. Por favor espere... + Estableciendo el fondo de pantalla. Por favor espere… Seguir sistema Oscuro Claro @@ -682,9 +691,9 @@ Modo de conexión limitada Imágenes de calidad Las imágenes de calidad son diagramas o fotografías que cumplen determinados estándares de calidad (mayormente de carácter técnico) y que son valiosas para proyectos de Wikimedia - Reanudando carga... - Pausando carga... - Cancelando carga... + Reanudando carga… + Pausando carga… + Cancelando carga… Cancelar carga Has habilitado el modo de conexión limitada. Todas las cargas están pausadas y se reanudarán cuando deshabilites este modo. El modo de conexión limitada está encendido. @@ -811,7 +820,8 @@ Guardar archivo GPX %d imagen seleccionada - %d imagenes seleccionadas + %d imágenes seleccionadas + %d imágenes seleccionadas Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado. Nota sobre cargas múltiples @@ -819,7 +829,7 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. - Cancelando todas las subidas... + Cancelando todas las subidas… Subidas Pendiente Falló diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index ff75cbc7f6..3dd463b345 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: Naturako elementuak (loreak, animaliak, mendiak) Objektu erabilgarriak (bizikletak, tren geltokiak) - Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat…) Mesedez EZ igo: Autorretratuak edo zure lagunen argazkiak Internetetik jaitsitako irudiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 841160581f..4cdd2b87a9 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -83,7 +83,7 @@ رمز عبور خودتان را فراموش کرده‌اید؟ ثبت نام واردشدن - شکیبا باشید... + شکیبا باشید… ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. @@ -122,7 +122,7 @@ تغییرها بارگذاری جستجوی رده‌ها - جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، ...) + جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، …) ذخیره تازه کردن فهرست @@ -411,7 +411,7 @@ شما هیچ اعلان خوانده‌شده‌ای ندارید نمایش دیده‌شده مشاهده خوانده نشده ها - لطفاً صبر کنید... + لطفاً صبر کنید… نمونه تصاویری که برای بازگذاری مناسب نیستند از این تصویر صرف نظر کن مدیریت تگ‌های EXIF @@ -423,7 +423,7 @@ مدل لنز شماره سریال نرم‌افزار - اشتراک از طریق... + اشتراک از طریق… اطلاعات عکس هیچ رده‌ای یافت نشد بارگذاری لغو شد @@ -455,7 +455,7 @@ به بوکمارک‌ها افزوده شد مشکل به وجود آمد. به عنوان پس‌زمینه انتخاب نشد. انتخاب به عنوان پس‌زمینه - قرار دادن پس‌زمینه. لطفاً صبر کنید... + قرار دادن پس‌زمینه. لطفاً صبر کنید… سامانه را دنبال کنید تیره روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 312ebc84c5..26328a3e2c 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -80,7 +80,7 @@ Kirjaudutaan Odota… Päivitetään kuvatekstejä ja kuvauksia - Odota... + Odota… Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. @@ -481,7 +481,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s - Jaa sovellus... + Jaa sovellus… Kuvan tiedot Luokkia ei löytynyt Kuvauksia ei löytynyt @@ -546,7 +546,7 @@ Lisätty kirjanmerkkeihin Jotain meni väärin. Ei voitu asettaa taustakuvaksi. Aseta taustakuvaksi - Asetetaan taustakuvaksi. Odota... + Asetetaan taustakuvaksi. Odota… Käytä järjestelmän Tumma Vaalea @@ -594,8 +594,8 @@ Rajoitettu yhteistila pois päältä. Jonossa olevat lähetykset kopioidaan nyt. Rajoitettu yhteystila Laatukuvat - Jatketaan lähettämistä... - Keskeytetään lähetys... + Jatketaan lähettämistä… + Keskeytetään lähetys… Peruutetaan tallennusta… Peruuta tallennus Rajoitettu yhteystila on päällä. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 992f418af0..995e4041b3 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -46,7 +46,7 @@ * Wladek92 * Y-M D --> - + Page Facebook de Commons Code source Github de Commons Logo de Commons @@ -70,31 +70,38 @@ Image du jour %1$d fichier en cours de téléversement + %1$d fichiers en cours de téléversement %1$d fichiers en cours de téléversement (%1$d) + (%1$d) (%1$d) Démarrage des téléversements %d téléversement en cours + %d téléversements en cours %d téléversements en cours %d téléversement + %d téléversements %d téléversements Cette image sera sous licence %1$s. + Ces images seront sous licence %1$s. Ces images seront sous licence %1$s. %1$d téléversement + %1$d téléversements %1$d téléversements - Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. + Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. + Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. Explorer @@ -113,9 +120,9 @@ Mot de passe oublié ? S’inscrire Connexion - Veuillez patienter... + Veuillez patienter… Mise à jour des légendes et des descriptions - Veuillez patienter... + Veuillez patienter… Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. @@ -185,7 +192,7 @@ Envoyer vos commentaires (par courriel) Aucun client de courriel installé Catégories récemment utilisées - En attente de première synchronisation... + En attente de première synchronisation… Vous n’avez encore téléchargé aucune photo. Réessayer Annuler @@ -225,7 +232,7 @@ Oui ! Davantage d’informations Catégories - Chargement en cours... + Chargement en cours… Aucune catégorie sélectionnée Aucune légende Aucune description @@ -521,7 +528,7 @@ Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images - Veuillez patienter... + Veuillez patienter… Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisies comme étant de la meilleure qualité pour le site. Les images téléversées par « Lieux à proximité » sont les images téléversées lors de la découverte de lieux sur la carte. Cette fonctionnalité permet aux contributeurs d’envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles ― en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff. @@ -543,7 +550,7 @@ Accès à l’emplacement du média refusé Nous ne pourrons pas obtenir automatiquement les données de localisation des images que vous téléchargez. Veuillez ajouter l’emplacement approprié pour chaque image avant de la soumettre. Téléversez des photos sur Wikimedia Commons directement depuis votre téléphone. Téléchargez l’application Commons maintenant : %1$s - Partager l’application via... + Partager l’application via… Informations sur l’image Aucune catégorie trouvée Aucun élément représenté trouvé @@ -570,6 +577,7 @@ Succès La catégorie %1$s est ajoutée. + Les catégories %1$s sont ajoutées. Les catégories %1$s sont ajoutées. Impossible d’ajouter des catégories. @@ -578,6 +586,7 @@ Modifier les éléments représentés L’élément représenté %1$s est ajouté. + Les éléments représentés %1$s sont ajoutés. Les éléments représentés %1$s sont ajoutés. Impossible d’ajouter des éléments représentés. @@ -620,7 +629,7 @@ Ajouté aux favoris Un problème est survenu. Impossible d’installer le fond d’écran. Définir comme fond d’écran - Installation du fond d’écran. Veuillez patienter... + Installation du fond d’écran. Veuillez patienter… Suivre le système Sombre Clair @@ -678,9 +687,9 @@ Mode de connexion limitée Images de qualité Les images de qualité sont des diagrammes ou des photographies qui respectent certains standards de qualité (qui sont, par nature, essentiellement techniques) et sont précieuses pour les projets Wikimedia. - Reprise du téléversement... - Mise en pause du téléversement... - Annulation du téléversement... + Reprise du téléversement… + Mise en pause du téléversement… + Annulation du téléversement… Annuler le téléversement Vous avez activé le mode de connexion limitée. Tous les téléversements sont suspendus et reprendront une fois ce mode désactivé. Le mode de connexion limitée est actif. @@ -809,6 +818,7 @@ Fichier GPX enregistré %d image sélectionnée + %d images sélectionnées %d images sélectionnées Souvenez-vous que toutes les images dans une importation multiple prennent les mêmes catégories et descriptions. Si les images de partagent pas les descriptions et catégories, veuillez effectuer plusieurs importations séparées. @@ -822,7 +832,7 @@ Autre problème ou information (merci d\'expliquer ci-dessous). Vos commentaires sont publiés sur la page wiki suivante : <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Êtes-vous sûr de vouloir annuler tous les téléchargements ? - Annulation de tous les téléchargements... + Annulation de tous les téléchargements… Téléversements En attente Échec diff --git a/app/src/main/res/values-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index b0ec664235..4659eecf1a 100644 --- a/app/src/main/res/values-gcr/strings.xml +++ b/app/src/main/res/values-gcr/strings.xml @@ -38,9 +38,9 @@ Ou bliyé ou Kodsigré ? Enskri oukò Konnègsyon - Souplé antann... + Souplé antann… Mizajou di léjann-yan ké dèskripsyon-yan - Souplé antann... + Souplé antann… Konnègsyon bon ! Konnègsyon pabon ! Fiché pa trouvé. Souplé éséyé ké rounòt. @@ -96,7 +96,7 @@ Enren ! Plis lenfòrmasyon Katégori-ya - Chajman ka fèt... + Chajman ka fèt… Pyès katégori sélègsyonnen Pyès léjann Pyès dèskripsyon diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1740c1890c..e11716a514 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -452,7 +452,7 @@ Modelo de lente Números de serie Software - Compartir a aplicación vía... + Compartir a aplicación vía… Información da imaxe Non se atoparon categorías Cancelouse a carga diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 50a04319b9..2375838538 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -39,7 +39,6 @@ %1$d फ़ाइलें अपलोड हो रहीं - \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -70,8 +69,8 @@ पासवर्ड भूल गये? खाता बनायें लॉग इन हो रहा है - कृपया प्रतीक्षा करें... - कृपया प्रतीक्षा करें... + कृपया प्रतीक्षा करें… + कृपया प्रतीक्षा करें… लॉग इन सफल! लॉग इन विफल! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। @@ -350,7 +349,7 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं... + सभी अपलोड रद्द किये जा रहे हैं… अपलोड लंबित विफल हुआ diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index d2d731c392..414f0dd40a 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -15,19 +15,22 @@ Slika dana Postavlja se %1$d datoteka + Postavlja se %1$d datoteke Postavljaju se %1$d datoteke - \@string/contributions_subtitle_zero %1$d postavljena datoteka + %1$d postavljena datoteke %1$d postavljene datoteke Započeto %1$d postavljanje + Započinjem %1$d postavljanja Započeta %1$d postavljanja %1$d postavljanje + %1$d postavljanja %1$d postavljanja Ova će slika biti licencirana pod %1$s @@ -46,7 +49,7 @@ Zaboravljena zaporka? Otvori račun Prijava - Molimo pričekajte ... + Molimo pričekajte … Prijava uspješna! Prijava neuspješna! Datoteka nije pronađena. Molimo probajte drugu. @@ -104,7 +107,7 @@ Pošaljite povratnu informaciju (putem elektroničke pošte) Klijent za elektroničku poštu nije instaliran Nedavno rabljene kategorije - Pričekajte za prvu sinkronizaciju... + Pričekajte za prvu sinkronizaciju… Nemate još postavljenih slika. Pokušaj ponovo Odustani @@ -144,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje... + Učitavanje… Ništa nije odabrano Nema opisa Nepoznata licencija @@ -193,7 +196,7 @@ Stranica datoteke na Zajedničkom poslužitelju Stavka na Wikidati Članak na Wikipediji - Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,… Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. Mogući problemi s ovom slikom: Slika je pretamna. Slika je mutna. @@ -281,7 +284,7 @@ Promijenio/la sam mišljenje, ne želim da više bude javno vidljivo Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće. Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo! - Molimo Vas, pričekajte ... + Molimo Vas, pričekajte … Preskoči ovu sliku Zadani jezik za opis Pokušavanje ažuriranja kategorija. @@ -293,7 +296,7 @@ Dodano u oznake Nešto je pošlo po zlu. Ne možemo postaviti pozadinu Postavi kao pozadinu - Postavljanje pozadine. Molimo, pričekajte... + Postavljanje pozadine. Molimo, pričekajte… Zadano Tamno Svijetlo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index aefc17d9d5..eb34386749 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -441,7 +441,7 @@ Sorozatszámok Szoftver Képek feltöltése Wikimedia Commons-ba közvetlenül a telefonodról. Töltsd le a Commons applikációt most: %1$s - Alkalmazás megosztása ezzel... + Alkalmazás megosztása ezzel… Képinformáció Nem található kategória Megszakított feltöltés @@ -474,7 +474,7 @@ Híd, múzeum, szálloda, stb. A belépés nem sikerült, kérj új jelszót. Beállítás háttérképnek - Beállítás háttérképnek. Kérem várjon... + Beállítás háttérképnek. Kérem várjon… Rendszerbeállítás követése Sötét Világos diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 219fa45210..8fff554e31 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,7 +61,6 @@ %1$d Unggahan - Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Jelajahi @@ -82,7 +81,7 @@ Memasuki log Silakan tunggu… Memperbarui takarir dan deskripsi - Mohon tunggu... + Mohon tunggu… Berhasil masuk log! Gagal masuk log! Berkas tidak ditemukan. Silakan coba berkas lain. @@ -191,7 +190,7 @@ Ya! Informasi selengkapnya Kategori - Memuat... + Memuat… Tidak ada yang dipilih Tanpa takarir Tidak ada keterangan @@ -497,7 +496,7 @@ Akses lokasi media ditolak Kami mungkin tidak dapat memperoleh data lokasi secara otomatis dari gambar yang Anda unggah. Harap tambahkan lokasi yang sesuai untuk setiap gambar sebelum mengirimkannya Mengunggah foto ke Wikimedia Commons secara langsung dari telepon Anda. Unduh aplikasi Commons sekarang: %1$s - Bagikan aplikasi lewat... + Bagikan aplikasi lewat… Info Gambar Kategori tidak ditemukan Penggambaran tidak ditemukan @@ -523,7 +522,6 @@ Pembaruan kategori Berhasil - Kategori %1$s ditambahkan. Kategori %1$s ditambahkan. Tidak bisa menambahkan kategori. @@ -569,7 +567,7 @@ Ditambahkan ke pembatas Terjadi kesalahan. Tidak bisa menetapkan wallpaper Jadikan Wallpaper - Sedang menetapkan Wallpaper. Tolong tunggu... + Sedang menetapkan Wallpaper. Tolong tunggu… Ikuti sistem Gelap Terang @@ -625,9 +623,9 @@ Mode Koneksi Terbatas Gambar Berkualitas Gambar berkualitas adalah diagram atau foto yang memenuhi standar kualitas tertentu (yang sifatnya teknis) dan berharga bagi proyek Wikimedia - Melanjutkan unggahan... - Menunda unggahan... - Membatalkan pengunggahan... + Melanjutkan unggahan… + Menunda unggahan… + Membatalkan pengunggahan… Batalkan pengunggahan Anda menyalakan mode koneksi terbatas. Semua pengunggahan ditunda dan akan dilanjutkan begitu Anda mematikan mode ini. Mode sambungan terbatas sedang menyala. @@ -743,7 +741,7 @@ %d gambar dipilih Bicara - Membatalkan semua unggahan... + Membatalkan semua unggahan… Unggahan Menunggu Gagal diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 51fe164419..994b1c3d34 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -70,9 +70,9 @@ Ka tu obliviis tua pasovorto? Enirar Eniranta - Voluntez vartar... + Voluntez vartar… Aktualiganta etiketi e deskripturi - Voluntez vartar... + Voluntez vartar… Eniro sucesoza! Eniro faliis! Arkivo ne trovita. Voluntez probar altr arkivo. @@ -142,7 +142,7 @@ Sendez komenti (per e-posto) Nula kliento di e-posto instalesis Kategorii recente uzita - Vartanta unesma sinkronigo... + Vartanta unesma sinkronigo… Vu ankore ne sendis fotografuri. Riprobar Nuligar @@ -180,7 +180,7 @@ Yes! Plusa informo Kategorii - Karganta... + Karganta… Nulo selektesis Nula deskripto-texto Nula deskripto @@ -410,7 +410,7 @@ Vu ne lektis irga avizo Vidar lektita Vidar ne-lektata - Vartez... + Vartez… Kopiita Exempli pri bona imaji por sendar a Commons Saltez ca imajo @@ -472,7 +472,7 @@ Ajusti Adjuntita marko-rubandi Uzar kom skreno-kovrilo - Kreanta skreno-kovrilo. Voluntez vartar... + Kreanta skreno-kovrilo. Voluntez vartar… Koloro obskura Koloro klara Charjez pluse @@ -500,7 +500,7 @@ Uzita Mea rango Imaji di qualeso - Nuliganta sendajo... + Nuliganta sendajo… Cesar kargajo Lektez pluse En omna idiomi diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 4176529534..ac64fbf2cd 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ * Sveinki * Sveinn í Felli --> - + Commons Facebook-síðan Grunnkóði Commons á Github Táknmerki Commons @@ -51,7 +51,7 @@ %1$d innsendingar - Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns + Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndaanna og gerð tækisins þíns Uppgötva @@ -138,7 +138,7 @@ Senda umsögn (með tölvupósti) Ekkert tölvupóstforrit er uppsett Nýlega notaðir flokkar - Bíð eftir fyrstu samstillingu... + Bíð eftir fyrstu samstillingu… Þú ert ekki ennþá búin(n) að senda inn neinar myndir. Reyna aftur Hætta við @@ -477,7 +477,7 @@ Hugbúnaður Aðgangi að staðsetningu gagnamiðla hafnað Sendu myndir inn á Wikimedia Commons beint úr símanum þínum. Sæktu Commons-appið núna: %1$s - Deila forriti með... + Deila forriti með… Upplýsingar í mynd Engir flokkar fundust Engar myndlýsingar fundust diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f408638708..e9aa8934ee 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -46,31 +46,38 @@ Foto del giorno %1$d file in caricamento + %1$d file in caricamento %1$d file in caricamento (%1$d) + (%1$d) (%1$d) Avvio del caricamento Elaborando %d caricamento + Elaborando %d caricamenti Elaborando %d caricamenti %d caricamento + %d caricamenti %d caricamenti Questa immagine sarà rilasciata in base alla licenza %1$s + Queste immagini saranno rilasciate in base alla licenza %1$s Queste immagini saranno rilasciate in base alla licenza %1$s %1$d caricamento + %1$d caricamenti %1$d caricamenti Ricezione di contenuti condivisi. L\'elaborazione dell\'immagine potrebbe richiedere del tempo a seconda delle dimensioni dell\'immagine e del dispositivo + Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Esplora @@ -516,7 +523,7 @@ Accesso alla posizione multimediale negato Potremmo non essere in grado di ottenere automaticamente i dati sulla posizione dalle immagini caricate. Si prega di aggiungere la posizione appropriata per ciascuna immagine prima di inviarla Carica foto su Wikimedia Commons direttamente dal tuo telefono. Scarica subito l\'app Commons: %1$s - Condividi applicazione tramite... + Condividi applicazione tramite… Informazioni sull\'immagine Nessuna categoria trovata Nessuna definizione trovata @@ -543,6 +550,7 @@ Successo Categoria %1$s aggiunta. + Categorie %1$s aggiunte. Categorie %1$s aggiunte. Non è stato possibile aggiungere le categorie. @@ -575,7 +583,7 @@ Esiste Necessita della fotografia Tipo di luogo: - Ponte, museo, albergo, ecc... + Ponte, museo, albergo, ecc… Si è verificato un errore durante l\'accesso. Devi reimpostare la password! MEDIA CLASSI FIGLIE @@ -588,7 +596,7 @@ Aggiungi ai preferiti Qualcosa è andato storto. Non è stato possibile impostare lo sfondo schermo Imposta come sfondo - Impostazione di sfondo in corso... + Impostazione di sfondo in corso… Segui sistema Scuro Chiaro @@ -758,6 +766,7 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata + %d immagini selezionate %d immagini selezionate Questo posto non ha ancora una foto, scattane una! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 0b512102b4..4b8c51f6c1 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -45,44 +45,37 @@ מועלה קובץ אחד מועלים %1$d קבצים - מועלים %1$d קבצים מועלים %1$d קבצים (%1$d) (%1$d) - (%1$d) (%1$d) ההעלאות מתחילות עיבוד העלאה עיבוד d% העלאות - עיבוד d% העלאות עיבוד d% העלאות העלאה אחת %d העלאות - %d העלאות %d העלאות התמונה הזאת תפורסם ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s - התמונות האלה תפורסמנה ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s העלאה אחת %1$d העלאות - %1$d העלאות %1$d העלאות מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך - מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך לחקור @@ -101,9 +94,9 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין... + נא להמתין… עדכון כיתובים ותיאורים - נא להמתין... + נא להמתין… הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -213,7 +206,7 @@ כן! מידע נוסף קטגוריות - בטעינה... + בטעינה… לא נבחר דבר אין כיתוב אין תיאור @@ -508,7 +501,7 @@ הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין... + נא להמתין… תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -530,7 +523,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך... + שיתוף היישום דרך… פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -558,7 +551,6 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. - נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -568,7 +560,6 @@ נוסף מוצג %1$s נוספו המוצגים %1$s - נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -611,7 +602,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין... + הגדרת טפט. נא להמתין… מערכת מעקב כהה בהירה @@ -671,7 +662,7 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה... + ביטול ההעלאה… ביטול ההעלאה הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. @@ -801,7 +792,6 @@ נבחרה תמונה אחת נבחרו שתי תמונות - נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -815,7 +805,7 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות... + ביטול כל ההעלאות… העלאות ממתינות נכשלו diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f20b986f82..f60bb30ddc 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -44,7 +44,6 @@ %1$d 件のファイルをアップロード中 - (%1$d) (%1$d) アップロードを開始中です @@ -55,14 +54,12 @@ %d 件のアップロード - この画像は%1$sライセンスのもとにアップロードされます これらの画像は%1$sライセンスのもとにアップロードされます %1$d 件のアップロード - 共有コンテンツを受信中です。 この画像の投稿の処理には、サイズやご使用の機器により時間がかかる事があります 共有コンテンツの受信中です。投稿画像の処理には、サイズやご使用の機器により時間がかかる事があります 探索 @@ -560,7 +557,7 @@ ブックマークに追加 問題が発生しました。壁紙を設定できませんでした。 壁紙として設定 - 壁紙を設定中。お待ちください... + 壁紙を設定中。お待ちください… システムのまま ダーク ライト diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 40eb016295..eb90e4a23c 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -47,8 +47,8 @@ Qqen Tettuḍ awal uffir? Jerred - Tuqqna... - Rǧu... + Tuqqna… + Rǧu… Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -100,7 +100,7 @@ Azen tikti (s yimayl) Ulac amsaɣ n yimayl ibedden Taggayin yettwasqedcenmelmi kan - Araǧu n umtawi amezwaru... + Araǧu n umtawi amezwaru… Ur tsuliḍ ara yakan tiwlafin. Ɛref̣ tikelt-nniḍen Sefsex @@ -130,7 +130,7 @@ Tɣileḍ igarrez? Ih! Taggayin - Asali... + Asali… Ula d yiwet ur tettwafren Ulac aglam Turagt tarussint diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index b729838b97..3703d373fb 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,28 +43,22 @@ 검색 뷰 오늘의 이미지 - %1$d개의 파일을 올리는 중 %1$d개의 파일을 올리는 중 - (%1$d) (%1$d) 파일 올리기 - %1$d장의 업로드를 처리하는 중입니다 %1$d장의 업로드를 처리하는 중입니다 - %d개 업로드 %d개 업로드 - 이 그림은 %1$s에 따라 사용이 허가됩니다 이 그림은 %1$s에 따라 사용이 허가됩니다 - %1$d개 업로드 %1$d개 업로드 찾아보기 @@ -85,7 +79,7 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오... + 기다려 주십시오… 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -456,7 +450,7 @@ 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오... + 기다려 주십시오… 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -471,7 +465,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유... + 앱 공유… 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -529,7 +523,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오... + 배경화면을 설정 중입니다. 기다려 주십시오… 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index b976848217..be63e9db5b 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -143,7 +143,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады... + Биринчи синхронизацияны сакълаб турады… Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -498,7 +498,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле... + Къошакъны буну бла юлюшле… Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +575,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +633,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады... - Джюклениу туракъланады... - Джюклениу ызына алынады... + Джюклениу андан ары бардырылады… + Джюклениу туракъланады… + Джюклениу ызына алынады… Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index 506e9e4b47..d9d5b65b91 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -70,8 +70,8 @@ Te şîfreya xwe ji bîr kir? Xwe tomar bike Têdikeve - Ji kerema xwe piçek bisekine ... - Xêra xwe hinek bisekine... + Ji kerema xwe piçek bisekine … + Xêra xwe hinek bisekine… Têketin bi ser ket! Têketin bi ser neket! Dosye nehat dîtin. Ji kerema xwe re dosyeyek din biceribîne. @@ -183,7 +183,7 @@ Wêneyên Barkirî Wêneyê din Belê, çima na - Ji kerema xwe piçek bisekine ... + Ji kerema xwe piçek bisekine … Wêne tevlî Wîkîpediyayê bike Tu dixwazî vê wêneyê tevlî gotara Wîkîpediyayê ya bi zimanê %1$s bikî? Pişrast bike diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index 8112afea61..ab657b354a 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -49,7 +49,7 @@ Юклев уьлгю: Дюр! Категориялар - Юклев... + Юклев… Бир зат сайланмагъан Тасвири ёкъ Пикирлешивлер ёкъ diff --git a/app/src/main/res/values-kus/strings.xml b/app/src/main/res/values-kus/strings.xml index 99fb8c1f74..02abd4ea10 100644 --- a/app/src/main/res/values-kus/strings.xml +++ b/app/src/main/res/values-kus/strings.xml @@ -62,9 +62,9 @@ Fʋ tami fʋ paaswɛɛtɛ? Yɔ\'ɔgin kpɛn\' Kpɛn\'ɛdnɛ - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Maligim maal pian\'azut nɛ pa\'alʋg nam - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Kpɛn\'ɛb nyaŋya Kpɛn\'ɛb gʋ\'ʋŋya M Pʋ nyɛ faal la. M bɛlimnɛ tiakim faal si\'a. @@ -169,7 +169,7 @@ Ɛɛn! Labaya bɛdigʋ Buudi kɔn\'ɔb-kɔn\'ɔb - Bɛ tʋʋma ni... + Bɛ tʋʋma ni… Pʋ gaŋ si\'ela Pian\'azug kae Pa\'alʋg kae @@ -400,7 +400,7 @@ Gɔsim dinɛ ka fʋ karim sa Gɔsim dinɛ ka fʋ nam pʋ karim Daʋŋʋ kidig footonam la nɔkirin - M bɛlimnɛ gu\'usim... + M bɛlimnɛ gu\'usim… Footo banɛ ka fʋ kpɛn\'ɛsi dɔlis zin\'ibanɛ be yamma anɛ footo banɛ ka fʋ kpɛn\'ɛs ka di yinɛ fʋn nyɛ di map ni la. Yaam paas media banɛ bɛ tuon Yaaiya @@ -418,7 +418,7 @@ Serial Numbers Software Pʋ bas suor ye fʋ kpɛn\' midia zin\'iginɛ - Pʋdigim app la dɔlis... + Pʋdigim app la dɔlis… Footo labaar Pʋ paam buudinama Pʋ nyɛ nwɛnnɛm si\'aa. @@ -492,7 +492,7 @@ Ba zaŋi paas bookmarknamin Daʋŋsi\'a naam. Pʋ nyaŋi maal nibdaa footo la Maalimi fʋ nindaa footo la - Maanɛ nindaa footo. M bɛlimnɛ gu\'usim... + Maanɛ nindaa footo. M bɛlimnɛ gu\'usim… Dɔl sistɛm la Lik Nɛɛsim @@ -538,9 +538,9 @@ Bas suor ye di tʋm saŋa bi\'ela! Atʋm bi\'ela zi\'esim Footo sʋma - Lɛm pin\'in kpɛn\'ɛsʋg... - Gu\'om kpɛn\'ɛsʋg... - Basid kpɛn\'ɛsʋg... + Lɛm pin\'in kpɛn\'ɛsʋg… + Gu\'om kpɛn\'ɛsʋg… + Basid kpɛn\'ɛsʋg… Basim kpɛn\'ɛsʋg Bas suor ye di tʋm saŋa bi\'ela. Nwɛnnɛm nam diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 2eb2fcf2f5..8b2ab6b955 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -21,7 +21,6 @@ %1$d файл жүктөлүүдө - Азырынча жүктөөлөр жок 1 жүктөө %1$d жүктөө @@ -137,7 +136,7 @@ Жүктөөнү жокко чыгаруу Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз Жүктөөнү улантуу - Күтө туруңуз... + Күтө туруңуз… Аталыш Сыпаттама Элементтер diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 9d69efabb7..d99e269ab1 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -61,7 +61,7 @@ Aloggen Waart wgl. … Beschrëftungen a Beschreiwungen aktualiséieren - Waart wgl. ... + Waart wgl. … Umeldung huet geklappt! D\'Aloggen huet net funktionéiert! Fichier net fonnt. Probéiert wgl. en anere Fichier. @@ -349,7 +349,7 @@ Déi geliese weisen Déi net geliese weisen Feeler beim Eraussiche vun de Biller - Waart wgl. ... + Waart wgl. … Kopéiert Beispiller vu gudde Biller fir op Commons eropzelueden Beispiller fir Biller, déi een net eropluede sollt @@ -361,7 +361,7 @@ Seriennummeren Software Luet Fotoen direkt vun Ärem Handy op Wikimedia Commons erop. Luet d\'Commons-App elo erof: %1$s - App deelen iwwer... + App deelen iwwer… Bildinformatiounen Keng Kategorie fonnt. Eroplueden ofgebrach @@ -411,7 +411,7 @@ Bei d\'Lieszeechen derbäigesat Et ass Eppes schif gaangen. D\'Hannergrondbild konnt net agestallt ginn Als Hannergrondbild festleeën - Hannergrondbild gëtt agestallt. Waart wgl... + Hannergrondbild gëtt agestallt. Waart wgl… System suivéieren Däischter Hell @@ -454,7 +454,7 @@ Limitéierte Verbindungsmodus Qualitéitsbiller Qualitéitsbiller sinn Diagrammen oder Fotoen, déi gewësse Qualitéitscritèren erfëllen (déi haaptsächlech vun technescher Natur sinn) a wäertvoll fir Wikimedia-Projete sinn. - Eropluede gëtt ofgebrach.... + Eropluede gëtt ofgebrach…. Eroplueden ofbriechen Kategoriesäit weisen Sprooch vum Interface vum Benotzer vun der App diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index f477ed8f0b..1720bfbcb4 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -33,8 +33,8 @@ Melj dich aan Wachwaord vergaete? Teiken dich in - Aan \'nt melje... - Wach estebleef... + Aan \'nt melje… + Wach estebleef… Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. @@ -88,7 +88,7 @@ Sjik feedback (mitten e-mail) Geine e-mailcliënt geïnstalleerd Recèntelik gebroekde categorieje - Oppe ieëste synchronisatie \'nt wachte... + Oppe ieëste synchronisatie \'nt wachte… Doe höbs nag gein plaetjes geüpload. Perbeer oppernuuj Braek aaf @@ -127,7 +127,7 @@ Versteis se \'t? Jao! Categorieje - \'nt laje... + \'nt laje… Geine gekaoze Gein besjrieving Ónbekande licentie diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 26a9bc7f77..cb7bebe41f 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -33,20 +33,27 @@ Dienos nuotrauka %1$d keliamas failas + %1$d keliami failai + %1$d failų keliamas %1$d keliami failai + %1$d įkėlimas + %1$d įkėlimai + %1$d įkėlimų - \@string/contributions_subtitle_zero - 1 įkėlimas Įkėlimai pradedami Pradedamas %1$d įkėlimas + Pradedami %1$d įkėlimai + Pradedami %1$d įkėlimų Pradedami %1$d įkėlimai %1$d įkėlimas + %1$d įkėlimai + %1$d įkėlimų %1$d įkėlimai Šio paveikslėlio licencija bus %1$s @@ -68,7 +75,7 @@ Jungiamasi Prašome palaukti… Antraštės ir aprašymai atnaujinami - Prašome palaukti... + Prašome palaukti… Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. @@ -172,7 +179,7 @@ Taip! Daugiau informacijos Kategorijos - Kraunasi... + Kraunasi… Niekas nepasirinkta Nėra antraštės Nėra aprašymo @@ -465,7 +472,7 @@ Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida - Prašome palaukti... + Prašome palaukti… Rinktinės nuotraukos yra aukštos kvalifikacijos fotografų ir iliustratorių vaizdai, kuriuos Vikiteka bendruomenė pasirinko kaip svetainėje aukščiausios kokybės. Vaizdai, įkelti per Netoliese esančias vietas, yra vaizdai, kurie įkeliami atrandant vietas žemėlapyje. Ši funkcija leidžia redaktoriams siųsti padėkos pranešimą naudotojams, kurie atlieka naudingus pakeitimus, naudojant nedidelę padėkos nuorodą istorijos puslapyje arba skirtumų puslapyje. @@ -486,7 +493,7 @@ Prieiga prie medijos vietos uždrausta Gali būti, kad negalėsime automatiškai gauti vietos duomenų iš jūsų įkeltų nuotraukų. Prieš pateikdami kiekvienai nuotraukai pridėkite tinkamą vietą Įkelkite nuotraukas į Vikiteką tiesiai iš savo telefono. Atsisiųskite Vikitekos programėlę dabar: %1$s - Dalintis programą per ... + Dalintis programą per … Vaizdo informacija Kategorijų nerasta Vaizdų nerasta @@ -746,7 +753,7 @@ Kita problema arba informacija (paaiškinkite toliau). Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> Ar tikrai norite atšaukti visus įkėlimus? - Atšaukiami visi įkėlimai... + Atšaukiami visi įkėlimai… Įkėlimai Laukiama Nepavyko diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 7a6d9e3628..9038eec9de 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,7 @@ Reģistrēties Pieslēdzas Lūdzu, uzgaidiet… - Lūdzu, uzgaidi... + Lūdzu, uzgaidi… Ieiešana veiksmīga Pieteikšanās neizdevās. Autentifikācija neizdevās! @@ -163,7 +163,7 @@ Nākamais attēls Skatīt arhivētos Skatīt nelasītos - Lūdzu, uzgaidiet... + Lūdzu, uzgaidiet… Izlaist šo attēlu Autors Autortiesības diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 916f4f4202..c496505ae9 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4,7 +4,7 @@ * Violetova * Vlad5250 --> - + Ризницата на Фејсбук Изворен код на Ризницата на Github Лого на Ризницата @@ -52,7 +52,7 @@ %1$d подигања - Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред + Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликите и вашиот уред Истражи @@ -73,7 +73,7 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте... + Почекајте… Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -479,7 +479,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте... + Почекајте… Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +501,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку... + Сподели преку… Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -780,7 +780,7 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања... + Ги откажувам сите подигања… Подигања Во исчекување Неуспешно diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index de888dcbc3..0d8e029a4c 100644 --- a/app/src/main/res/values-mni/strings.xml +++ b/app/src/main/res/values-mni/strings.xml @@ -18,7 +18,7 @@ ꯈꯨꯠꯌꯦꯛ ꯄꯤꯈꯠꯂꯨ ꯃꯅꯨꯡ ꯆꯪꯁꯤꯟꯂꯤ ꯉꯥꯏꯍꯥꯛ ꯉꯥꯏꯕꯤꯌꯨ - ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ... + ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ… ꯃꯥꯏꯄꯥꯛꯅꯥ ꯆꯪꯁꯤꯜꯂꯦ ꯫ ꯆꯪꯁꯤꯟꯕ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ! ꯐꯥꯏꯜ ꯊꯤꯕꯥ ꯐꯪꯗꯔꯦ ꯫ ꯆꯥꯟꯕꯤꯗꯨꯅꯥ ꯑꯇꯣꯞꯄ ꯑꯃꯥ ꯇꯧꯕꯤꯔꯣ ꯫ @@ -59,7 +59,7 @@ ꯍꯣꯏ! ꯑꯍꯦꯟꯕ ꯋꯥꯔꯣꯜ ꯃꯆꯥꯈꯥꯏꯕꯁꯤꯡ - ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ..... + ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ….. ꯑꯃꯠꯇ ꯈꯟꯗꯦ ꯑꯀꯨꯞꯄ ꯃꯔꯣꯜ ꯌꯥꯎꯗꯦ ꯈꯟꯅ-ꯅꯩꯅꯕ ꯂꯩꯇꯦ diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index a6c18bca30..27a76b0a75 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -45,7 +45,7 @@ ဝိုတ်စ မအက္ခရ်ပၞုက် ပတိုန် စၟတ်သမ္တီ လုပ်လံက်အေန် ဒၟံင် - ပဂုန်တုဲ မင်မွဲလစုတ်... + ပဂုန်တုဲ မင်မွဲလစုတ်… လုက်အေန် အာစိုပ်ဒတုဲ! လံက်အေန် လီုလာ်! ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။ @@ -148,7 +148,7 @@ ယွံ! ဆက်လဴ ပရူတင်ဂၞင် ကဏ္ဍဂမၠိုင် - ပတိုန်ဒၟံင်... + ပတိုန်ဒၟံင်… ဟွံမဲကဵု ပရေၚ်ရုဲစှ် ဟွံမဲကဵု က္ဍိုပ်လိက် ဟွံမဲကဵု ဗမံက်ထ္ၜး diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 546b43f4fa..9655945855 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -17,7 +17,6 @@ %1$d संचिका अपभारीत होत आहे - अद्याप अपभारणे नाहीत एक अपभारण %1$d अपभारणे @@ -94,7 +93,7 @@ प्रतिसाद पाठवा (विपत्राद्वारे) कोणतेही ईमेल क्लायंट स्थापित नाहीत अलीकडे वापरलेले वर्ग - प्रथम संकालनाची प्रतीक्षा करीत आहे ... + प्रथम संकालनाची प्रतीक्षा करीत आहे … आपण अद्याप काहीच चित्रे अपभारीत केली नाहीत. पुन्हा प्रयत्न करा रद्द करा diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 1fce0c0daf..e5dd0f3beb 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -19,20 +19,16 @@ အားလုံး ယနေ့အတွက် အထူးဓာတ်ပုံ - ဖိုင် %1$d ခု တင်နေသည် ဖိုင် %1$d ခု တင်နေသည် အပ်ပလုဒ်များ စတင်ခြင်း - %1$d ခု တင်ထားသည် %1$d ခု တင်ထားသည် - ဤရုပ်ပုံသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် ဤရုပ်ပုံများသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် - %1$d အက်ပလုပ် %1$d အက်ပလုပ်များ ရှာဖွေစူးစမ်းပါ @@ -49,9 +45,9 @@ အကောင့်ဝင်ရန် စကားဝှက် မေ့နေပါသလား မှတ်ပုံတင်ရန် - လော့ဂ်အင် ဝင်ရောက်နေသည်... - ခေတ္တစောင့်ပါ... - ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + လော့ဂ်အင် ဝင်ရောက်နေသည်… + ခေတ္တစောင့်ပါ… + ကျေးဇူးပြု၍ ခဏစောင့်ပါ… လော့အင် အောင်မြင်သည် လော့အင် မအောင်မြင်ပါ ဖိုင်မတွေ့ပါ၊ အခြးဖိုင်တစ်ခု စမ်းကြည့်ပါ။ @@ -133,7 +129,7 @@ ဟုတ်ကဲ့ သတင်းအချက်အလက် ပို၍ ကဏ္ဍများ - ဝန်ဆွဲတင်နေသည်... + ဝန်ဆွဲတင်နေသည်… ဘာမှရွေးချယ်မထားပါ ပုံစာ မရှိ ဖော်ပြချက် မရှိ @@ -315,7 +311,7 @@ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ရုပ်ပုံများကိုရွေးနေစဉ် အမှားဖြစ်ပွားခဲ့ပါသည် - ကျေးဇူးပြု၍ ခဏစောင့်ပါ... + ကျေးဇူးပြု၍ ခဏစောင့်ပါ… နမူနာရုပ်ပုံများ အက်ပလုပ်တင်ရန် မဟုတ်ပါ ဤရုပ်ပုံအား ကျော်သွားမည် ဒေါင်းလုဒ် မအောင်မြင်ပါ။ ပြင်ပသိုလှောင်မှုခွင့်ပြုချက်မရှိဘဲ ဖိုင်ဒေါင်းလုဒ်မဆွဲနိုင်ပါ။ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index bf971f6bcc..3b4bf30dc9 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -522,7 +522,7 @@ Toegang tot medialocatie geweigerd Het is mogelijk dat we niet automatisch locatiegegevens kunnen verkrijgen van foto\'s die u uploadt. Voeg de locatie bij elke foto toe voordat u die upload Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s - App delen via... + App delen via… Afbeeldingsinfo Geen categorieën gevonden Geen beschrijvingen gevonden @@ -599,7 +599,7 @@ Als bladwijzer toegevoegd Er is iets fout gegaan. Kan de achtergrond niet instellen Instellen als achtergrond - Wordt ingesteld als achtergrond. Een ogenblik geduld... + Wordt ingesteld als achtergrond. Een ogenblik geduld… Systeem volgen Donker Licht @@ -659,7 +659,7 @@ Kwaliteitsafbeeldingen zijn diagrammen of foto\'s die voldoen aan bepaalde kwaliteitsnormen (die meestal technisch van aard zijn) en waardevol zijn voor Wikimedia-projecten Uploaden hervatten… Uploaden onderbreken… - Uploaden wordt geannuleerd... + Uploaden wordt geannuleerd… Uploaden Annuleren U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. Beperkte verbindingsmodus is ingeschakeld. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 7e11ea03a3..62e01d4d56 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -51,9 +51,9 @@ ߌ ߓߘߊ߫ ߢߌ߬ߣߊ߬ ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊ߫؟ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߝߍ߬ߛߓߍߟߌ ߣߌ߫ ߞߊ߲߬ߛߓߍߟߌ ߟߊߞߎߘߦߊ ߦߴߌ ߘߐ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫߹ ߞߐߕߐ߮ ߡߊ߫ ߛߐ߬ߘߐ߲߬. ߘߏ߫ ߜߘߍ߫ ߡߊߝߍߣߍ߲߫ ߖߊ߰ߣߌ߲߫. @@ -69,7 +69,7 @@ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲߬ ߞߐ߯ߟߕߊ ߟߎ߬ - ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫... + ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫… ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫ %1$d%% ߓߘߊ߫ ߘߝߊ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ @@ -148,7 +148,7 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -408,7 +408,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +418,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +486,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +533,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c097898e9f..eab67e0766 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -81,7 +81,7 @@ Mandar vòstres comentaris (per corrièl) Cap de client de corrièl pas installat Categorias utilizadas recentament - Espèra de primièra sincronizacion... + Espèra de primièra sincronizacion… Avètz pas encara telecargat cap de fòto. Tornar ensajar Anullar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8c64900a56..d0ee733960 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -8,7 +8,7 @@ * Sony dandiwal * ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ --> - + ਕਾਮਨਜ਼ ਮਾਰਕਾ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ @@ -17,11 +17,10 @@ ਸਾਰੇ ਦਿਨ ਦੀ ਤਸਵੀਰ - ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ + ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ %1$d ਫ਼ਾਈਲਾਂ ਚੜ੍ਹਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ - \@string/contributions_subtitle_zero %1$d upload %1$d ਅੱਪਲੋਡ @@ -30,7 +29,7 @@ %1$d ਸ਼ੁਰੂ ਹੋ ਰਹੇ ਹਨ - &d ਅੱਪਲੋਡ + %1$d ਅੱਪਲੋਡ %1$d ਅੱਪਲੋਡਾਂ ਇਹ ਤਸਵੀਰ ਦਾ %1$s ਹੇਠ ਲਸੰਸ ਜਾਰੀ ਕੀਤੀ ਜਾਵੇਗਾ @@ -45,7 +44,7 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -129,7 +128,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ... + ਲੱਦ ਰਿਹਾ ਹੈ… ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -201,7 +200,7 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ ਲਿਖਤ ਛਾਪੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index dcd8ea284a..09132f40e1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -508,7 +508,7 @@ Zobacz przeczytane Wyświetl nieprzeczytane Wystąpił błąd podczas pobierania zdjęć - Proszę czekać... + Proszę czekać… Polecane zdjęcia to zdjęcia wysoko wykwalifikowanych fotografów i ilustratorów, które społeczność Wikimedia Commons wybrała jako jedne z najwyższych jakości na stronie. Obrazy przesłane przez Pobliskie miejsca to obrazy, które są przesyłane przez odkrywanie miejsc na mapie. Ta funkcja umożliwia redaktorom wysyłanie powiadomień z podziękowaniem do użytkowników, którzy dokonują przydatnych zmian - za pomocą małego linku z podziękowaniem na stronie historii lub na stronie diff. diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 9c257c2735..b7449e9571 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -477,7 +477,7 @@ Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance - Ch\'a l\'abia passiensa... + Ch\'a l\'abia passiensa… Le fòto an evidensa a son ëd plance fàite da dij fotògraf e ilustrator motobin àbij che la comunità ëd Wikipedia Commons a l\'ha sernù tra cole ëd qualità pi àuta an sël sit. Le plance carià dai pòst ëd prossimità a son le plance carià con la dëscuverta dij pòst an sla carta. Costa fonsionalità a përmet ai contributor ëd mandé na notìfica d\'aringrassiament a j\'utent ch\'a fan dle modìfiche ùtij - an dovrand na cita liura d\'aringrassiament an sla pàgina dla stòria o cola dle diferense. @@ -499,7 +499,7 @@ Acess a la locassion dël mojen arfudà I podoma pa oten-e an automàtich ij dàit ëd localisassion dle plance che chiel a caria. Për piasì, ch\'a giontà la posission apropià për tute le plance prima ëd mandeje Ch\'a caria dle fòto su Wikimedia Commons diretaman da sò teléfon. Ch\'a dëscaria l\'aplicassion Commons adess: %1$s - Partagé l\'aplicassion via... + Partagé l\'aplicassion via… Anformassion an sla plancia Gnun-e categorìe trovà Gnun-e descrission trovà @@ -776,7 +776,7 @@ Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota). Ij sò sugeriment a saran giontà a coste pàgine wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> É-lo sigur ëd vorèj anulé tuti ij cariament? - Anulament ëd tuti ij cariament... + Anulament ëd tuti ij cariament… Cariament An atèisa Falì diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 4f17da26f7..461cb6b1d8 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -65,7 +65,7 @@ CC BY 3.0 هو وېشنيزې - رابرسېرېږي... + رابرسېرېږي… هېڅ هم نه دی ټاکل شوی څرگندونه نشته نامعلوم جواز diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3779a8a516..b0dd3b016a 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,7 +28,7 @@ * Tuliouel * YuriNikolai --> - + Página do Commons no Facebook Código fonte do Commons no Github Logotipo do Commons @@ -51,32 +51,39 @@ Estado do local Imagem do Dia - carregando arquivo + carregando arquivo + carregando %1$d arquivos carregando %1$d arquivos (%1$d) + (%1$d) (%1$d) Iniciando carregamentos Processando %d carregamento + Processando %d carregamentos Processando %d carregamentos %d carregamento + %d carregamentos %d carregamentos Esta imagem será licenciada sob %1$s + Estas imagens serão licenciadas sob %1$s Estas imagens serão licenciadas sob %1$s %1$d carregamento + %1$d carregamentos %1$d carregamentos - Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo + Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo + Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Explorar @@ -95,9 +102,9 @@ Esqueceu a senha? Cadastre-se Efetuar login - Por favor, aguarde... + Por favor, aguarde… Atualizando legendas e descrições - Por favor, aguarde... + Por favor, aguarde… Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. @@ -161,7 +168,7 @@ Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"%1$s\">publicação no GitHub</a> para informar erros e sugestões. - Politica de privacidade + Política de privacidade Créditos Sobre Enviar comentários (por e-mail) @@ -248,7 +255,7 @@ Ponte de Arco-Íris Tulipa Bem-vindo à Wikipédia - Direitos de autor são bem vindo + Direitos de autor são bem-vindo Ópera de Sydney Cancelar Abrir @@ -306,7 +313,7 @@ Commons Avalie-nos Perguntas frequentes - Guia de usuario + Guia de usuário Pular Tutorial A Internet não está disponível Erro ao tentar obter as notificações @@ -521,7 +528,7 @@ Acesso à localização da mídia negado É possível que não possamos obter automaticamente os dados de localização das imagens que você carregar. Por favor adicione a localização adequada para cada imagem antes de envia-las Carregue fotos na wiki Wikimedia Commons, diretamente do seu celular. Baixe o aolicativo Commons agora: %1$s - Compartilhar aplicativo via... + Compartilhar aplicativo via… Informação da imagem Nenhuma categoria encontrada Nenhuma representação encontrada @@ -548,6 +555,7 @@ Sucesso A categoria %1$s foi adicionada. + As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -556,6 +564,7 @@ Editar representações O elemento retratado %1$s está adicionado. + Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -767,6 +776,7 @@ Salvar arquivo GPX %d imagem selecinada + %d imagens selecionadas %d imagens selecionadas Escreva algo sobre o item %1$s. Isso será visivel publicamente. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index bad9dc5005..19a52d72ff 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,7 @@ * Unamane * Vitorvicentevalente --> - + Página da wiki Commons no Facebook Código-fonte da wiki Commons no Github Logótipo da wiki Commons @@ -44,31 +44,38 @@ Imagem do Dia a carregar %1$d ficheiro + a carregar muitos %1$d ficheiros a carregar %1$d ficheiros (%1$d) + (%1$d) (%1$d) A iniciar carregamentos A processar %d carregamento + A processar %d carregamentos A processar %d carregamentos %d carregamento + %d carregamentos %d carregamentos Esta imagem será licenciada com a %1$s + Estas imagens serão licenciadas com a %1$s Estas imagens serão licenciadas com a %1$s %1$d carregamento + %1$d carregamentos %1$d carregamentos - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo A receber conteúdo partilhado. O processamento das imagens pode demorar algum tempo, dependendo do tamanho das mesmas e do seu dispositivo Explorar @@ -156,8 +163,8 @@ Política de privacidade Créditos Sobre - Enviar comentários (por correio eletrónico) - Não foi instalado nenhum cliente de correio eletrónico + Enviar comentários (por correio eletrónico) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. @@ -276,7 +283,7 @@ Gravar as fotografias tiradas com a câmara da aplicação no armazenamento do seu dispositivo Inicie sessão na sua conta Enviar ficheiro de registos - Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas + Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas Não foi encontrado nenhum navegador da Internet para abrir o URL Erro! Não foi possível encontrar o URL Nomear para eliminação @@ -491,7 +498,7 @@ Ver lidas Ver não lidas Ocorreu um erro ao escolher imagens - Aguarde, por favor... + Aguarde, por favor… As fotografias destacadas são imagens de fotógrafos e ilustradores altamente qualificados, que a comunidade da wiki Wikimedia Commons escolheu como as de melhor qualidade do \'\'site\'\'. As imagens carregadas via \"Locais próximos\" são as imagens que são carregadas descobrindo locais do mapa. Esta funcionalidade permite que os editores enviem uma notificação de agradecimento aos utilizadores que fizerem edições úteis - usando uma pequena hiperligação de agradecimento na página do historial ou na de diferenças. @@ -513,7 +520,7 @@ Acesso à localização de multimédia negado Podemos não conseguir obter automaticamente os dados de localização das fotografias que carregar. Adicione a localização apropriada de cada fotografia antes de a enviar, por favor Carregue fotografias na wiki Wikimedia Commons, diretamente do seu telemóvel. Descarregue a aplicação Commons agora: %1$s - Partilhar aplicação por... + Partilhar aplicação por… Informação da imagem Não foi encontrada nenhuma categoria Não foi encontrada nenhuma representação @@ -540,6 +547,7 @@ Êxito A categoria %1$s foi adicionada. + As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -548,6 +556,7 @@ Editar elementos retratados O elemento retratado %1$s está adicionado. + Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -589,7 +598,7 @@ Adicionado aos marcadores Ocorreu um problema. Não foi possível definir a imagem de fundo Definir como imagem de fundo - A definir a imagem de fundo. Aguarde, por favor... + A definir a imagem de fundo. Aguarde, por favor… Seguir sistema Escuro Claro @@ -645,8 +654,8 @@ Modo de ligação limitada Imagens de qualidade As imagens de qualidade são diagramas ou fotografias que satisfazem certos padrões de qualidade (principalmente de natureza técnica) e são valiosos para projetos da Wikimedia - A retomar carregamento... - A pausar carregamento... + A retomar carregamento… + A pausar carregamento… A cancelar o carregamento… Cancelar carregamento Ativou o modo de ligação limitada. Todos os carregamentos foram colocados em pausa e serão retomados quando desativar este modo. @@ -709,7 +718,7 @@ Não foi encontrada nenhuma localização Que tal adicionar o local onde a imagem foi tirada?\nOs dados de localização ajudam os editores da wiki a encontrarem a sua fotografia, tornando-a muito mais útil.\nObrigado! Adicionar localização - Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. + Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. Detalhes As realizações só estão disponíveis na versão de produção; consulte a documentação para programadores, por favor. A tabela de classificação só está disponível na versão prod. Consulte a documentação do desenvolvedor. @@ -760,6 +769,7 @@ Erro no envio de agradecimento ao autor. %d imagem selecionada + %d imagens selecionadas %d imagens selecionadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 0bcbc15506..ad1d0b805f 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,8 @@ %1$d de fișiere se încarcă - \@string/contributions_subtitle_zero (%1$d) + (%1$d) (%1$d) Pornirea încărcărilor @@ -74,8 +74,8 @@ V-ați uitat parola? Înregistrare Se conectează - Vă rugăm să așteptați ... - Vă rugăm să așteptați ... + Vă rugăm să așteptați … + Vă rugăm să așteptați … Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. @@ -458,7 +458,7 @@ Vezi citit Vezi necitit A apărut o eroare la alegerea imaginilor - Vă rugăm să așteptați ... + Vă rugăm să așteptați … Imaginile de Calitate sunt imagini ale unor fotografi și ilustratori de înaltă calificare, pe care comunitatea Wikimedia Commons a ales-o ca fiind de cea mai înaltă calitate pe site. Imaginile Încărcate prin Locurile din Apropiere sunt imaginile care sunt încărcate prin descoperirea locurilor de pe hartă. Această caracteristică permite editorilor să trimită o notificare de Mulțumire utilizatorilor care fac modificări utile - folosind un mic link de mulțumire pe pagina istoric sau pe pagina dif. @@ -478,7 +478,7 @@ Numere Serie Software Încărcați fotografii pe Wikimedia Commons direct de pe telefon. Descărcați aplicația Commons acum: %1$s - Partajează aplicația prin ... + Partajează aplicația prin … Informații despre imagine Nu s-au găsit categorii Nu s-au Găsit Reprezentări diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b15d777876..861d7ee27c 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ * ЛингвоЧел * ОйЛ --> - + Facebook-страница Викисклада Исходный код Викисклада на гитхабе Логотип Викисклада @@ -105,7 +105,7 @@ %1$d загрузок - Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства + Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства @@ -556,7 +556,7 @@ Отказано в доступе к местоположению файла Возможно, мы не сможем автоматически получать данные о местоположении из загруженных вами изображений. Пожалуйста, добавьте подходящее место для каждого изображения перед отправкой Загружайте фото на Викисклад прямо с телефона. Скачайте приложение Wikimedia Commons прямо сейчас: %1$s - Поделиться приложением с помощью... + Поделиться приложением с помощью… Информация об изображении Категории не найдены. Описания не найдены @@ -637,7 +637,7 @@ Добавлено в закладки Что-то пошло не так. Не удалось установить фоновую заставку Сделать фоновой заставкой - Идёт установка фоновой заставки... + Идёт установка фоновой заставки… Настройки системы Тёмная Светлая @@ -695,8 +695,8 @@ Режим ограниченного подключения Качественные изображения Качественные изображения - это диаграммы или фотографии, которые соответствуют определенным стандартам качества (которые в основном носят технический характер) и представляют ценность для проектов Викимедиа - Возобновление загрузки... - Приостановка загрузки... + Возобновление загрузки… + Приостановка загрузки… Отмена загрузки… Отменить загрузку Вы включили ограниченный режим подключения. Все загрузки приостановлены и возобновятся после отключения этого режима. @@ -841,7 +841,7 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок... + Отмена всех загрузок… Загрузки В ожидании Не удалось diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index 08f9a1fecc..d4b5916593 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -169,7 +169,7 @@ ھا! وڌيڪ معلومات زمرا - لاهيندي... + لاهيندي… ڪوبہ چونڊيل ناھي عنوان ناهي ڪا تشريح ناھي @@ -315,7 +315,7 @@ لينس ماڊل سيريل انگ سافٽويئر - ايپ ذريعي ونڊيو... + ايپ ذريعي ونڊيو… عڪس معلومات زمرا نہ لڌا رد-ڪيل چاڙھ diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index 78114e3362..0489c363a1 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -44,9 +44,9 @@ Vajáldahttetgo beassansáni? Searvva Čáliha sisa - Vuordil... + Vuordil… Ođasmáhttá govvateavsttaid ja govvádusaid - Vuordil... + Vuordil… Sisačáliheapmi lihkostuvai! Sisačáliheapmi ii lihkostuvvan! Fiila ii gávdnon. Geahččal áinnas eará fiilla. @@ -112,7 +112,7 @@ Atte máhcahaga (e-poasttain) Ii leat ásahuvvon epoastadoaimmaheaddji Áitto geavahuvvon kategoriijat - Vuordime vuosttaš synkroniserema... + Vuordime vuosttaš synkroniserema… It leat vel bajásluđen ovttage gova. Geahččal ođđasit Gaskkalduhte @@ -143,7 +143,7 @@ Jua! Lassedieđut Kategoriijat - Luđeme... + Luđeme… Ii guhtege válljejuvvon Ii leat govvateaksta Ii gávdno govvádus diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 5997677efd..8e9cde75c9 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -112,7 +112,7 @@ Pošaljite Vašu povratnu informaciju (putem e-pošte) Nemate uspostavljen klijent za e-poštu Nedavno korištene kategorije - Čekam prvo usklađivanje... + Čekam prvo usklađivanje… Još uvijek niste otpremili nijednu sliku. Pokušaj ponovo Otkaži @@ -147,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje... + Učitavanje… Ništa nije odabrano Nema opisa Nema razgovora diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 92fa25f3eb..0e661acb74 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -5,25 +5,24 @@ * Sandaru * හරිත --> - + කොමන්ස් ෆේස්බුක් පිටුව කොමන්ස් ලාන්චනය කොමන්ස් වෙබ් අඩවිය - 1 ගොනුවක් උඩුගත කෙරේ + 1 ගොනුවක් උඩුගත කෙරේ ගොනු %d ක් උඩුගත කෙරේ - තවමත් කිසිදු උඩුගත කිරීමක් නැත - එක් උඩුගත කිරීමක් ඇත + එක් උඩුගත කිරීමක් ඇත උඩුගත කිරීම් %1$d ක් ඇත - 1 උඩුගත කිරීමක් ආරම්භ කරමින් + 1 උඩුගත කිරීමක් ආරම්භ කරමින් උඩුගත කිරීම් %1$d ක් ආරම්භ කරමින් - 1 උඩුගත කිරීමක් + 1 උඩුගත කිරීමක් උඩුගත කිරීම් %1$d ක් මෙම පින්තූරය %1$s යටතේ වලංගු වනු ඇත diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 49fc88a3b0..99a0bf5483 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -491,7 +491,7 @@ Zobraziť prečítané Zobraziť neprečítané Nastala chyba pri vyberaní obrázkov - Čakajte, prosím... + Čakajte, prosím… Najlepšie obrázky sú fotografie od vysoko skúsených fotografov a ilustrátorov, ktoré vybrala komunita Wikimedie Commons ako jedny z najkvalitnejších na stránke. Obrázky nahrané cez Miesta v okolí sú obrázky, ktoré sú nahrané vďaka objavovaniu miest na mape. Táto funkcia umožňuje poslať poďakovanie za užitočné úpravy používateľom – použitím malého odkazu poďakovať v histórií stránky alebo na stránke rozdielu medzi revíziami. @@ -513,7 +513,7 @@ Prístup k polohe médií bol odmietnutý Možno nebudeme môcť automaticky získať údaje o polohe z obrázkov, ktoré nahráte. Pred odoslaním, prosím, pridajte ku každému obrázku údaj o polohe. Nahrávajte fotky na Wikimedia Commons priamo z vášho mobilu. Stiahnite si aplikáciu Wikimedia Commons teraz: %1$s - Zdieľať aplikáciu cez... + Zdieľať aplikáciu cez… Informácie o obrázku Nenájdené žiadne kategórie Neboli nájdené spôsoby vykreslovania @@ -593,7 +593,7 @@ Pridané do záložiek Niečo sa pokazilo. Tapetu sa nepodarilo nastaviť Nastaviť ako tapetu - Nastavujem tapetu. Prosím, čakajte... + Nastavujem tapetu. Prosím, čakajte… Predvolený systém Tmavý Svetlý @@ -651,9 +651,9 @@ Mód limitovaného pripojenia Kvalitné obrázky Kvalitné obrázky sú diagramy a fotografie, ktoré spĺňajú určité štandardy (ktoré sú väčšinou technického charakteru) a sú cenné pre projekty Wikimédie - Pokračovanie nahrávania... - Pozastavovanie nahrávania... - Prerušovanie nahrávania... + Pokračovanie nahrávania… + Pozastavovanie nahrávania… + Prerušovanie nahrávania… Zrušiť nahrávanie Zapli ste mód limitovaného pripojenia. Všetky nahrávania budú teraz pozastavené a budú pokračovať až po vypnutí tohto módu. Mód limitovaného pripojenia je zapnutý. @@ -787,7 +787,7 @@ Iný problém alebo informácia (vysvetlite nižšie). Vaša spätná väzba sa zverejní na nasledujúcej wiki stránke: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Ste si istí, že chcete zrušiť všetky nahrávania? - Ruším všetky nahrávania... + Ruším všetky nahrávania… Nahrané súbory Čakajúce Zlyhané diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 61531980f1..b91c3c0b13 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ * McDutchie * Upwinxp --> - + Facebook stran Zbirke Izvorna koda Zbirke v shrambi Github Logotip Zbirke @@ -66,8 +66,8 @@ %1$d nalaganj - Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. - Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. @@ -87,9 +87,9 @@ Ste pozabili geslo? Ustvari račun Prijavljanje - Prosimo, počakajte ... + Prosimo, počakajte … Posodabljam napise in opise - Prosimo, počakajte ... + Prosimo, počakajte … Uspešno ste se prijavili! Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. @@ -133,7 +133,7 @@ Spremembe Naloži Poišči kategorije - Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) + Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, …) Shrani Osveži Seznam @@ -159,7 +159,7 @@ Pošljite povratno informacijo (prek e-pošte) Nameščen ni noben e-poštni odjemalec Pred kratkim uporabljene kategorije - Čakam na prvo sinhronizacijo ... + Čakam na prvo sinhronizacijo … Naložili niste še nobene fotografije. Poskusi znova Prekliči @@ -199,7 +199,7 @@ Da! Več informacij Kategorije - Nalaganje ... + Nalaganje … Nič ni izbrano Ni napisa Ni opisa @@ -491,7 +491,7 @@ Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake - Prosimo, počakajte ... + Prosimo, počakajte … Izbrane slike so slike izvrstnih fotografov in ilustratorjev, ki jih je skupnost Wikimedijine zbirke prepoznala kot najbolj kakovostne v tem projektu. Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. @@ -513,7 +513,7 @@ Dostop do lokacije predstavnosti zavrnjen Za slike, ki jih nalagate, ne moremo samodejno pridobiti lokacije. Pred pošiljanjem dodajte za vsako sliko ustrezno lokacijo. Nalagajte slike v Wikimedijino zbirko neposredno iz telefona. Prenesite aplikacijo Commons zdaj: %1$s - Deli aplikacijo prek ... + Deli aplikacijo prek … Informacije o sliki Ni najdenih kategorij Ni najdenih upodobitev @@ -569,7 +569,7 @@ Koordinat ni bilo mogoče pridobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise - Deli slike prek ... + Deli slike prek … Ničesar še niste prispevali %s ni opravil_a še nobenega prispevka Račun ustvarjen! @@ -593,7 +593,7 @@ Dodano med zaznamke Nekaj je šlo narobe. Ozadja ni bilo mogoče nastaviti. Nastavi kot ozadje - Nastavljam ozadje. Prosimo, počakajte ... + Nastavljam ozadje. Prosimo, počakajte … Sledi sistemu Temna Svetla @@ -649,9 +649,9 @@ Način omejene povezanosti Kakovostne slike Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie - Nalaganje se nadaljuje ... - Zaustavljam nalaganje ... - Preklicujem nalaganje ... + Nalaganje se nadaljuje… + Zaustavljam nalaganje… + Preklicujem nalaganje… Preklic nalaganja Vklopili ste način omejene povezanosti. Vsa nalaganja so začasno ustavljena in se bodo nadaljevala, ko boste ta način izklopili. Način omejene povezanosti je vklopljen. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index fe70bc6b6f..f1e7412d48 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -34,32 +34,39 @@ Слика дана %1$d датотека се отпрема + %1$d датотеке се отпремају %1$d датотеке се отпремају %1$d отпремање + %1$d отпремања %1$d отпремања Покретање отпремања Процесуирање %d отпремање + Процесуирање %d отпремања Процесуирање %d отпремања %d отпремање + %1$d отпремања %d отпремања Слика ће се водити под лиценцом %1$s + Слике ће се водити под лиценцом %1$s Слике ће се водити под лиценцом %1$s %1$d отпремање + %1$d отпремања %1$d отпремања - Примање дељеног садржаја... Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја - Примање дељеног садржаја... Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја Истрага Изглед @@ -496,7 +503,7 @@ Приступ локацији медија је одбијен Можда нећемо моћи да аутоматски прибавимо податке о локацији из слика које отпремите. Додајте одговарајућу локацију за сваку слику пре објављивања Отпреми фотографије на Викимедијину Оставу директно са свог телефона. Преузми апликацију Оставе сада: %1$s - Подели апликацију преко... + Подели апликацију преко… Информације о слици Нису пронађене категорије Отказано отпремање @@ -521,12 +528,13 @@ Успешно Категорија %1$s је додата. + Категорије %1$s су додате. Категорије %1$s су додате. Није могуће додати категорије. Ажурирај категорију Уреди приказе - Ажурирање координата... + Ажурирање координата… Ажурирање координата Ажурирање описа Ажурирање натписа @@ -729,6 +737,7 @@ Чување GPX датотеке %d слика је одабрана + %d слике су одабране %d слика је одабрано Унесите коментар diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 79ae5ea28f..64379ac928 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -26,32 +26,25 @@ Togel ka Luhur Gambar poé ieu - ngunjal %1$d berkas ngunjal %1$d berkas - (%1$d) (%1$d) Mitembeyan Ngamuat - Ngolah %d muatan Ngolah %d muatan - %1$d muatan %1$d muatan - Ieu gambar bakal dilisénsi %1$s Ieu gambar bakal dilisénsi %1$s - %1$d Dimuat %1$d Dimuat - Nampa kontén anu dibagikeun. Ngolah gambarna bisa jadi rada lila gumantung kana ukuran gambar jeung gaway anjeun Nampa kontén anu dibagikeun Langlang @@ -71,7 +64,7 @@ Asup log Tungguan… Nganyarkeun pertélaan jeung pedaran - Mangga tungguan... + Mangga tungguan… Laksana login! Gagal login! Berkas teu kapanggih. Coba berkas séjén. @@ -399,7 +392,7 @@ Tempo arsip Tempo nu can dibaca Éror pas keur nyomot gambar - Mangga tungguan... + Mangga tungguan… Iwalkeun ieu gambar Karya Hak cipta @@ -409,7 +402,7 @@ Nomer Seri Sopwér Muat poto ka Wikimedia Commons langsung tina ponsél. Unduh Commons App ayeuna: %1$s - Bagikeun app liwat... + Bagikeun app liwat… Info Gambar Euweuh Kategori kapanggih Muatan bedo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 370bf0915f..c49d64ad22 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -506,7 +506,7 @@ Åtkomst till mediaplats nekades Vi kanske inte automatiskt kan få platsdata från bilder du laddar upp. Lägg till lämplig plats för varje bild innan du skickar in Ladda upp foton till Wikimedia Commons direkt från din telefon. Ladda ned Commons-appen nu: %1$s - Dela appen via... + Dela appen via… Bildinfo Inga kategorier hittades Inga beskrivningar hittades @@ -783,7 +783,7 @@ Andra problem eller information (ange nedan). Din återkoppling kommer att skickas till följande wikisida: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a> Är du säker på att du vill avbryta alla uppladdningar? - Avbryter alla uppladdningar... + Avbryter alla uppladdningar… Uppladdningar Pågår Misslyckades diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index f9162bc7b4..4f41da5f6d 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -98,7 +98,7 @@ பின்னூட்டம் அனுப்பு (மின்னஞ்சல் வழியாக) மின்னஞ்சற் செயலி எதுவும் நிறுவப்படவில்லை அண்மையிற் பயன்படுத்தப்பட்ட பகுப்புகள் - முதல் ஒத்திசைவுக்காக காத்திருக்கிறது ... + முதல் ஒத்திசைவுக்காக காத்திருக்கிறது … நீர் இன்னும் எவ்வொளிப்படத்தையும் பதிவேற்றவில்லை. மீண்டும் முயல்க கைவிடு @@ -131,7 +131,7 @@ ஆம்! மேலதிக தகவல்கள் பகுப்புகள் - ஏற்றப்படுகிறது... + ஏற்றப்படுகிறது… தெரிவு செய்யப்படவில்லை தலைப்பு இல்லை விளக்கம் இல்லை diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index add46f7b7b..13ee985b95 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -110,7 +110,7 @@ ಇರೆನ ಅಬಿಪ್ರಾಯೊ ಬರೆಲೆ(ಮಿಂಚಂಚೆ). ಇರೆನ ಮಿಂಚಂಚೆ ಇಜ್ಜಿ. ಇಂಚಿಗ್ ಸೃಷ್ಟಿ ಮಾಲ್ತಿನ ವರ್ಗೊ. - ಒಂತೆ ಸಮಯ ಕಾಯೊಡು.... + ಒಂತೆ ಸಮಯ ಕಾಯೊಡು…. ಇರ್ ಒಂಜಿಲಾ ಪಟೋನ್ ಅಪ್ಲೋಡ್ ಮಾಲ್ತಿಜ್ಜಿ. ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ @@ -336,7 +336,7 @@ ಅನುರಕ್ಷಿತ ತೂಲೆ ಓದಂದಿನ ತೂಲೆ ಆಕೃತಿಲೆನ್ ಪೆಜ್ಜಿನಗ ದೋಷ ಆಂಡ್ - ದಯಮಲ್ತ್ ಕಾಪುಲೆ... + ದಯಮಲ್ತ್ ಕಾಪುಲೆ… ಸಂಯೋಜನೆಲು ಸೂಚನೆಲು ನನಾತ್ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index ae80a53357..0c478b2214 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -127,7 +127,7 @@ ఫీడుబ్యాకును పంపండి (ఈమెయిలు ద్వారా) ఈమెయిలు క్లయంటేదీ లేదు ఇటీవల వాడిన వర్గాలు - మొట్టమొదటి సింక్ కోసం చూస్తున్నాం... + మొట్టమొదటి సింక్ కోసం చూస్తున్నాం… ఇంకా మీరు ఫోటోలేమీ ఎక్కించలేదు. మళ్ళీ ప్రయత్నించు రద్దుచేయి @@ -457,7 +457,7 @@ క్రమ సంఖ్యలు సాఫ్టువేరు నేరుగా మీ ఫోను నుంచే వికీమీడియా కామన్స్‌కు ఫోటోలను ఎక్కించండి. కామన్స్ యాప్‌ను ఇప్పుడే దించుకోండి: %1$s - యాప్‌ను దీని ద్వారా పంచుకోండి... + యాప్‌ను దీని ద్వారా పంచుకోండి… బొమ్మ సమాచారం వర్గాలేమీ కనబడలేదు ఎక్కింపును రద్దు చేసాం @@ -523,7 +523,7 @@ బుక్‌మార్కులకు చేర్చాం ఏదో లోపం జరిగింది. వాల్‌పేపరును సెట్ చెయ్యలేకపోయాం వాల్‌పేపరుగా అమర్చు - వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి... + వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి… నల్లటి వెలుగుతో స్థానపు సెట్టింగులను తెరవడం విఫలమైంది. స్థానాన్ని మానవికంగా ఆన్ చెయ్యండి @@ -576,9 +576,9 @@ పరిమిత కనెక్షను మోడ్‌ను అచేతనం చేసాం. పెండింగులో ఉన్న ఎక్కింపులు తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ నాణ్యమైన బొమ్మలు - ఎక్కింపును తిరిగి మొదలెడుతున్నాం... - ఎక్కింపును నిలుపుతున్నాం... - ఎక్కింపును రద్దు చేస్తున్నాం... + ఎక్కింపును తిరిగి మొదలెడుతున్నాం… + ఎక్కింపును నిలుపుతున్నాం… + ఎక్కింపును రద్దు చేస్తున్నాం… ఎక్కింపును రద్దుచెయ్యి మీరు పరిమిత కనెక్షను మోడ్‌ను చేతనం చేసారు. ఎక్కింపులన్నీ నిలిచిపోయాయి. మీరు ఈ మోడ్‌ను అచేతనం చెయ్యగానే అవి తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ ఆన్ అయింది. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 125bba590f..70bee59ef5 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -38,21 +38,16 @@ รูปภาพประจำวัน กำลังอัปโหลดไฟล์ %1$d ไฟล์ - \@string/contributions_subtitle_zero - (%1$d) (%1$d) กำลังเริ่มอัปโหลด - กำลังเริ่มอัปโหลด %1$d รายการ กำลังเริ่มอัปโหลด %1$d รายการ - การอัปโหลด %1$d รายการ การอัปโหลด %1$d รายการ - ภาพนี้จะอยู่ในสัญญาอนุญาต %1$s ภาะเหล่านี้จะอยู่อยู่ในสัญญาอนุญาติ %1$s สำรวจ @@ -398,7 +393,7 @@ รุ่นเลนส์ หมายเลขซีเรียล ซอฟต์แวร์ - แบ่งปันแอปผ่าน... + แบ่งปันแอปผ่าน… ไม่พบหมวดหมู่ ภาพเซลฟี ภาพเบลอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 31a9f0b530..79482fb4e9 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -210,7 +210,7 @@ Evet! Daha Fazla Bilgi Kategoriler - Yükleniyor... + Yükleniyor… Hiçbir şey seçilmedi Altyazı yok Açıklama yok @@ -505,7 +505,7 @@ Okunanları görüntüle Okunmayanları görüntüle Resimler seçilirken hata oluştu - Lütfen bekleyin... + Lütfen bekleyin… Seçkin resimler, Wikimedia Commons topluluğunun sitedeki en yüksek kaliteden bazıları olarak seçtiği son derece yetenekli fotoğrafçıların ve illüstratörlerin görüntüleridir. Yakındaki yerler üzerinden yüklenen resimler, haritadaki yerleri keşfederek yüklenen resimlerdir. Bu özellik, editörlerin, geçmiş sayfasında veya fark sayfasında küçük bir teşekkür bağlantısı kullanarak faydalı düzenlemeler yapan kullanıcılara bir Teşekkür bildirimi göndermesine olanak tanır. @@ -604,7 +604,7 @@ Yer işaretlerine eklendi Bir şeyler yanlış gitti. Duvar kağıdı ayarlanamadı Duvar kağıdı olarak ayarla - Duvar Kağıdı ayarlanıyor. Lütfen bekleyin... + Duvar Kağıdı ayarlanıyor. Lütfen bekleyin… Sistemi izle Koyu Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 9e821ae24d..cc2343a771 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ * Ата * Пан Хаунд --> - + Facebook-сторінка Вікісховища Програмний код Вікісховища на GitHub Логотип Вікісховища @@ -81,7 +81,7 @@ %1$d завантажень - Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою + Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою @@ -612,7 +612,7 @@ Додано у закладки Щось трапилось. Не вдалося встановити шпалери робочого столу Встановити в якості шпалер робочого столу - Встановлення робочого столу. Будь ласка зачекайте... + Встановлення робочого столу. Будь ласка зачекайте… На взірець системи Темна Світла diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index a708c873a2..f09f76ac62 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -73,9 +73,9 @@ Parolni unutdingizmi? Roʻyxatdan oʻtish Kirish - Iltimos kuting... + Iltimos kuting… Sarlavhalar va tavsiflarni yangilash - Iltimos, kutib turing... + Iltimos, kutib turing… Kirish muvaffaqiyatli bajarildi! Kirish muvaffaqiyatsiz yakunlandi! Fayl topilmadi. Iltimos, boshqa faylni izlab koʻring. @@ -180,7 +180,7 @@ Ha! Batafsil maʼlumot Turkumlar - Yuklanmoqda... + Yuklanmoqda… Tanlanmagan Izoh yoʻq Tavsif yoʻq @@ -390,7 +390,7 @@ Xatchoʻplar Xatchoʻplar Bajarildi - Iltimos, kuting... + Iltimos, kuting… EXIF teglarni boshqarish Muallif Mualliflik huquqlari diff --git a/app/src/main/res/values-vec/strings.xml b/app/src/main/res/values-vec/strings.xml index bbcb64561a..52bb495ea8 100644 --- a/app/src/main/res/values-vec/strings.xml +++ b/app/src/main/res/values-vec/strings.xml @@ -68,7 +68,7 @@ Cargamento de %1$s no riusio Schicia par vixuałixare I me ultimi cargamenti - In coa... + In coa… Fałimento %1$d%% conpleto Drio cargar.. @@ -114,7 +114,7 @@ Mandane on comento (co ła mail) Nisun client de posta eletronega instałà Categorie doparà ultimamente - Speta par ła prima sincronixasion... + Speta par ła prima sincronixasion… No te ghe njiancora cargà na foto Riproa Descançełare @@ -403,7 +403,7 @@ Varda no lexeste Varda no lexeste Se ga vuo on eror co se jera drio ełexare łe imajini. - Speta on fià... + Speta on fià… Le foto in primo pian łe xé imajini de fotografi altamente cuałifegai che ła comunità de Wikimedia Commons ła ga ełeto come fotografi de alta cuałità sol sito. Imajini cargae via \"Posti cuà rente\", imajini che łe njien cargae scoerxendo posti n\'te ła mapa Sta funsion ła consente ai editori de enviar na notifega de ringrasiamento ai uxuari che i fa modifeghe che serve, doparando on lingambo picenin de ringrasiamento n\'te ła pajina del storego o n\'te ła pajina de łe difarense.\n\nQuesta funzione consente agli editor di inviare una notifica di ringraziamento agli utenti che apportano modifiche utili, utilizzando un piccolo link di ringraziamento nella pagina della cronologia o nella pagina delle differenze. @@ -421,7 +421,7 @@ Numari seriałi Software Carga foto so Wikimedia Commons diretamente dal to tełefonin. Descarga l\'aplicasion deso: %1$s - Spartisi aplicasion co... + Spartisi aplicasion co… Informasion so l\'imajine Nisuna categoria catada Cargamento nułà @@ -461,7 +461,7 @@ Xonta ai favorii Calcosa el xé ndà roerso. No xé sta pusibiłe canbiar el sfondo Inposta el sfondo - Drio inpostar el sfondo. Speta on fià... + Drio inpostar el sfondo. Speta on fià… Segui el sistema Scuro Ciaro diff --git a/app/src/main/res/values-xal/strings.xml b/app/src/main/res/values-xal/strings.xml index 346ff15e17..c362060613 100644 --- a/app/src/main/res/values-xal/strings.xml +++ b/app/src/main/res/values-xal/strings.xml @@ -23,15 +23,15 @@ Вики-аһулх һазр Тохрллһ Вики-аһулх һазрур ацалх - Ацалгдҗана... + Ацалгдҗана… Кергләчин нерн Нууц үг Невтрх Нууц үгән мартвт? Бүрткүлх Невтрҗәнә - Күләхнтн... - Күләхнтн... + Күләхнтн… + Күләхнтн… Невтрлт амҗлтта болла! Невтрҗ чадсн уга! Ацаллт кеҗ экллә! @@ -83,7 +83,7 @@ Тиим Делгрңгү Нерн, төрл - Умшҗана... + Умшҗана… Алькинь чигн суңһад уга Тодрхаллт уга Күүндән уга diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index b32eeb0057..7927da1af5 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -61,7 +61,7 @@ ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა - ეთმიხარგუ... + ეთმიხარგუ… მახვარებუშ ჯოხო პაროლი გენშართით თქვანი პროფილით Commons Beta-შა @@ -71,7 +71,7 @@ სისტემაშა მიშულა ქორთხინთ ქჷმიცადით … მუკნაჭარეფი დო ეჭარუეფი მითმიახალებუ - ქორთხინთ ქჷმიცადით... + ქორთხინთ ქჷმიცადით… სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. @@ -181,7 +181,7 @@ ქოǃ უმოსი ინფორმაცია კატეგორიეფი - იხარგუ... + იხარგუ… მუთუნ ვა რე გიშაგორილი მუკნაჭარა ვა რე ვა რე ეჭარუა diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index 27080b9991..c6f27bb991 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -13,7 +13,7 @@ ⵜⴻⵜⵜⵓⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵣⵔⴰⵢ? ⵣⵎⵎⴻⵎ ⴷⴰ ⵜⴽⵛⵛⵎⴷ - ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ... + ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ… ⴰⴽⵛⴰⵎ !ⵉⵎⵓⵔⵙ ⴰⴽⵛⴰⵎ ⵉⵣⴳⵍ! ⴰⴼⴰⵢⵍⵓ ⵓⵔ ⵉⵜⵜⵢⵓⴼⴰ. ⴰⵎⵓⵔ ⵏⵏⴽ ⴰⵔⵎ ⴰⴼⴰⵢⵍⵓ ⵢⴰⴹⵏ. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 74ff641c0b..2a307e955c 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,28 +79,22 @@ 地点状态 今日图片 - %1$d个文件正在上传 %1$d个文件正在上传 - %1$d次上传 %1$d次上传 开始上传 - 正在处理%d个上传 正在处理%d个上传 - %d个上传 %d个上传 - 该图像的授权协议是 %1$s 这些图像的授权协议是 %1$s - %1$d次上传 %1$d次上传 @@ -552,7 +546,7 @@ 已拒绝访问媒体位置 我们可能无法自动从你上传的图片中获取位置数据。提交前请为每张图片添加适当的位置 直接在您手机上的维基共享资源应用中上传照片。立即下载共享资源应用:%1$s - 分享到... + 分享到… 图像信息 找不到分类 找不到描写。 @@ -578,7 +572,6 @@ 分类更新 成功 - 分类%1$s已添加。 分类%1$s已添加。 无法添加分类。 @@ -586,7 +579,6 @@ 正在尝试更新描述。 编辑描述 - 已添加 %1$s 个描写。 已添加 %1$s 个描写。 无法添加描述。 @@ -687,8 +679,8 @@ 限制连接模式 优良图片 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值 - 正在恢复上传... - 暂停上传... + 正在恢复上传… + 暂停上传… 正在取消上传… 取消上传 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。 @@ -816,7 +808,6 @@ 正在保存KML文件 正在保存GPX文件 - 已选择%d个图像 已选择%d个图像 请记住,每次多图片上传会为其中的所有图片标注相同的分类和描述。如果这些图片并不共享同样的描述和分类,请分别进行多次上传。 @@ -830,7 +821,7 @@ 其他问题或信息(请在下方解释)。 您的反馈已经发布在以下wiki页面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> 您确定要取消所有上传吗? - 取消所有的上传... + 取消所有的上传… 上传 待处理 失败 From 197855af0e58bf89bbc1b49877639a77a6331145 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 28 Oct 2024 13:02:08 +0100 Subject: [PATCH 006/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ab/strings.xml | 4 +- app/src/main/res/values-af/strings.xml | 3 +- app/src/main/res/values-anp/strings.xml | 10 ++--- app/src/main/res/values-ar/strings.xml | 8 ++-- app/src/main/res/values-as/strings.xml | 4 +- app/src/main/res/values-ast/strings.xml | 4 +- app/src/main/res/values-az/strings.xml | 2 +- .../main/res/values-b+roa+tara/strings.xml | 6 +-- app/src/main/res/values-b+sr+Latn/strings.xml | 19 +++------ app/src/main/res/values-ba/strings.xml | 8 ++-- app/src/main/res/values-ban/strings.xml | 6 +-- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-blk/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 6 --- app/src/main/res/values-bs/strings.xml | 5 +-- app/src/main/res/values-ca/strings.xml | 8 +--- app/src/main/res/values-ce/strings.xml | 8 ++-- app/src/main/res/values-cs/strings.xml | 15 +------ app/src/main/res/values-csb/strings.xml | 6 +-- app/src/main/res/values-cy/strings.xml | 19 --------- app/src/main/res/values-da/strings.xml | 10 ++--- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-diq/strings.xml | 8 ++-- app/src/main/res/values-el/strings.xml | 8 ++-- app/src/main/res/values-eo/strings.xml | 16 ++++---- app/src/main/res/values-es/strings.xml | 38 +++++++----------- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 10 ++--- app/src/main/res/values-fi/strings.xml | 10 ++--- app/src/main/res/values-fr/strings.xml | 39 ++++++++----------- app/src/main/res/values-gcr/strings.xml | 6 +-- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 7 ++-- app/src/main/res/values-hr/strings.xml | 17 ++++---- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 18 +++++---- app/src/main/res/values-io/strings.xml | 14 +++---- app/src/main/res/values-is/strings.xml | 8 ++-- app/src/main/res/values-it/strings.xml | 15 ++----- app/src/main/res/values-iw/strings.xml | 26 +++++++++---- app/src/main/res/values-ja/strings.xml | 5 ++- app/src/main/res/values-kab/strings.xml | 8 ++-- app/src/main/res/values-ko/strings.xml | 35 +++++++++++++++-- app/src/main/res/values-krc/strings.xml | 14 +++---- app/src/main/res/values-ku/strings.xml | 6 +-- app/src/main/res/values-kum/strings.xml | 2 +- app/src/main/res/values-kus/strings.xml | 18 ++++----- app/src/main/res/values-ky/strings.xml | 3 +- app/src/main/res/values-lb/strings.xml | 12 +++--- app/src/main/res/values-li/strings.xml | 8 ++-- app/src/main/res/values-lt/strings.xml | 21 ++++------ app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 12 +++--- app/src/main/res/values-mni/strings.xml | 4 +- app/src/main/res/values-mnw/strings.xml | 4 +- app/src/main/res/values-mr/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 14 ++++--- app/src/main/res/values-nl/strings.xml | 9 +++-- app/src/main/res/values-nqo/strings.xml | 20 +++++----- app/src/main/res/values-oc/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 13 ++++--- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 9 +++-- app/src/main/res/values-ps/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++-------- app/src/main/res/values-pt/strings.xml | 32 ++++++--------- app/src/main/res/values-ro/strings.xml | 10 ++--- app/src/main/res/values-ru/strings.xml | 14 +++---- app/src/main/res/values-sd/strings.xml | 4 +- app/src/main/res/values-se/strings.xml | 8 ++-- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-si/strings.xml | 11 +++--- app/src/main/res/values-sk/strings.xml | 14 +++---- app/src/main/res/values-sl/strings.xml | 30 +++++++------- app/src/main/res/values-sr/strings.xml | 19 +++------ app/src/main/res/values-su/strings.xml | 13 +++++-- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tcy/strings.xml | 4 +- app/src/main/res/values-te/strings.xml | 12 +++--- app/src/main/res/values-th/strings.xml | 7 +++- app/src/main/res/values-tr/strings.xml | 6 +-- app/src/main/res/values-uk/strings.xml | 6 +-- app/src/main/res/values-uz/strings.xml | 8 ++-- app/src/main/res/values-vec/strings.xml | 10 ++--- app/src/main/res/values-xal/strings.xml | 8 ++-- app/src/main/res/values-xmf/strings.xml | 6 +-- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 20 ++++++++-- 90 files changed, 445 insertions(+), 480 deletions(-) diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 22f382f576..9ff1b19b4c 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭаларҭа Иҟаҵатәуп арегистрациа Асистемахь аҭаларҭа - Шәааԥшы ԥыҭрак… + Шәааԥшы ԥыҭрак... Аҭалара қәҿиарала имҩаԥысит! Асистемахь аҭалараан агха! Афаил ԥшаам. Даҽа фаилк шәахәаԥш. @@ -64,7 +64,7 @@ Ари шәара еилышәкаама? Ааи! Акатегориақәа - Аҭагалара… + Аҭагалара... Акагь алхӡам Иҟам ахҳәаа Идырым алицензиа diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 57ba77cc93..1da8b3101f 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -22,6 +22,7 @@ %1$d lêers aan die uploaden + \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -147,7 +148,7 @@ Ja! <u>Meer inligting</u> Kategorieë - Laai … + Laai ... Niks gekies nie Geen beskrywing Geen bespreking nie diff --git a/app/src/main/res/values-anp/strings.xml b/app/src/main/res/values-anp/strings.xml index e4029af9b0..70a01949f2 100644 --- a/app/src/main/res/values-anp/strings.xml +++ b/app/src/main/res/values-anp/strings.xml @@ -27,14 +27,14 @@ पासवर्ड भूलाय गेलौ की? साइन अप करौ प्रवेश होय रहलौ छौं - कृपया प्रतीक्षा करौ… - कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ... प्रवेश विफल अपलोड आरंभ! हाल केरौ अपलोड कतारबद्ध विफल - अपलोड होय रहलौ छौं… + अपलोड होय रहलौ छौं... ठामे मँ हमरौ अपलोड साझा करौ @@ -68,7 +68,7 @@ हाँव! बेसी जानकारी श्रेणी सिनी - लोड होय रहलौ छौं… + लोड होय रहलौ छौं... कुछु चयनित नाय कोय शीर्षक नाय कोय विवरण नाय @@ -173,7 +173,7 @@ पूर्ण होलौं अगलका छवि हाँव, केन्हअ नाय - कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ... प्रतिलिपि बनैलौ गेलै! लेखक स्थान diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b0fda69907..46ffcb7f56 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار… + يرجى الانتظار... نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -530,7 +530,7 @@ عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار… + الرجاء الانتظار... الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +552,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر… + مشاركة التطبيق عبر... معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -695,7 +695,7 @@ وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل … + جاري استئناف التحميل ... جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 63aa12b964..960b55bdad 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -27,7 +27,7 @@ পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক লগইন হৈ আছে - অনুগ্ৰহ কৰি অপেক্ষা কৰক… + অনুগ্ৰহ কৰি অপেক্ষা কৰক... লগইন সফল হ\'ল! লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। @@ -74,7 +74,7 @@ <u>গোপনিয়তা নীতি</u> প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই - প্ৰথম চিংকৰ বাবে অপেক্ষাৰত… + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 34212aebbc..df61ed0610 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -74,7 +74,7 @@ Aniciando sesión Espera… Actualizando pies y descripciones - Porfavor espera… + Porfavor espera... ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. @@ -480,7 +480,7 @@ Númberos de serie Software Xubi semeyes a Wikimedia Commons direutamente dende\'l to móvil. Descarga yá la app de Commons: %1$s - Compartir l\'aplicación per… + Compartir l\'aplicación per... Información de la imaxe Nun s\'alcontró nenguna categoría Nun s\'alcontraron retratos diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index d2ea468ad7..1edbe43fce 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -104,7 +104,7 @@ CC BY 3.0 Əlavə məlumat Kateqoriyalar - Yüklənir… + Yüklənir... Heç biri seçilməmişdir Naməlum lisenziya Yenilə diff --git a/app/src/main/res/values-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 8e77643235..4fa660ef88 100644 --- a/app/src/main/res/values-b+roa+tara/strings.xml +++ b/app/src/main/res/values-b+roa+tara/strings.xml @@ -40,8 +40,8 @@ Tràse Passuord scurdate? Reggistrate - Stoche a tràse… - Aspitte… + Stoche a tràse... + Aspitte... E\' trasute! Non g\'è trasute! File non acchiate. Pruève \'n\'otre file. @@ -121,7 +121,7 @@ Permesse richieste Non ge tìne notifeche non lette Errore assute mendre ca ste pigghiave le immaggine - Aspitte… + Aspitte... Zumbe ste immaggine Autore Lènghe d\'a descrizione predefinite diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index b8b602d0dc..cd1cb09e8f 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -5,7 +5,7 @@ * Milicevic01 * Zoranzoki21 --> - + Fejsbuk stranica Ostave Izvorni kod na Github-u Logo Ostave @@ -26,39 +26,32 @@ Slika dana %1$d datoteka se otprema - %1$d datoteke se otpremaju %1$d datoteke se otpremaju %1$d otpremanje - %1$d otpremanja %1$d otpremanja Pokretanje otpremanja Procesuiranje %d otpremanje - Procesuiranje %d otpremanja Procesuiranje %d otpremanja %d otpremanje - %d otpremanja %d otpremanja Slika će se voditi pod licencom %1$s - Slike će se voditi pod licencom %1$s Slike će se voditi pod licencom %1$s %1$d otpremanje - %1$d otpremanja %1$d otpremanja - Primanje deljenog sadržaja… Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja - Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja - Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja... Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja + Primanje deljenog sadržaja... Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja Istraga Izgled @@ -493,7 +486,7 @@ Pristup lokaciji medija je odbijen Možda nećemo moći da automatski pribavimo podatke o lokaciji iz slika koje otpremite. Dodajte odgovarajuću lokaciju za svaku sliku pre objavljivanja Otpremi fotografije na Vikimedijinu Ostavu direktno sa svog telefona. Preuzmi aplikaciju Ostave sada: %1$s - Podeli aplikaciju preko… + Podeli aplikaciju preko... Informacije o slici Nisu pronađene kategorije Otkazano otpremanje @@ -518,13 +511,12 @@ Uspešno Kategorija %1$s je dodata. - Kategorije %1$s su dodate. Kategorije %1$s su dodate. Nije moguće dodati kategorije. Ažuriraj kategoriju Uredi prikaze - Pokušavanje promena koordinata… + Pokušavanje promena koordinata... Ažuriranje koordinata Ažuriranje opisa Ažuriranje natpisa @@ -706,7 +698,6 @@ Nije moguće podeliti ovu stavku %d slika je odabrana - %d slika je odabrano %d slika je odabrano diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 4c33b396fe..0fc68329f1 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -61,9 +61,9 @@ Серһүҙҙе оноттоғоҙмо? Теркәлеү Системаға инеү - Зинһар, көтөгөҙ… + Зинһар, көтөгөҙ... Аңлатмалар һәм тасуирламалар яңыртыла - Зинһар, көтөгөҙ… + Зинһар, көтөгөҙ... Системаға инеү уңышлы! Системаға инеү уңышһыҙ! Файл табылманы. Башҡа файлды эҙләп ҡарағыҙ. @@ -131,7 +131,7 @@ Фекереңде ебәр (эл.почта аша) Почта клиенты асыҡланмаған Яңыраҡ ҡулланылған категориялар - Тәүге синхронлаштырыуҙы көтөү… + Тәүге синхронлаштырыуҙы көтөү... Әлегә бер фото ла йөкләмәгәнһегеҙ Ҡабатларға Кире алыу @@ -171,7 +171,7 @@ Эйе! Ентеклерәк Категориялар - Йөкләнә башланы… + Йөкләнә башланы... Бер ни ҙә һайланмаған Тасуирламаһы юҡ Фекер алышыу юҡ diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index 1273eaf0d6..b24bc00221 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -61,7 +61,7 @@ Lali kruna Sandi? Daftar Ngeranjingin log - Jantos dumun… + Jantos dumun... Nganyarin sesirah miwah pidarta Jantos dumun… Mahasil manjing log! @@ -303,7 +303,7 @@ Nomor seri Piranti lunak Unggah foto nuju Wikimédia Commons langsung saking télépon ragané. Unduh aplikasi Commons mangkin: %1$s - Wedar aplikasi saking… + Wedar aplikasi saking... Pidarta Gambar Pangunggahan Kawangdé %1$s kaunggah olih: %2$s @@ -340,7 +340,7 @@ Kaanggén Paringkat Titiang Kualitas Gambar - Ngalanturang unggahan… + Ngalanturang unggahan... Ngarérénang unggahan… Wangdé Unggah Lisénsi Média diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cb19d6e39f..6ee9315423 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -304,7 +304,7 @@ Преглеждане на прочетени Преглеждане на непрочетени Възникна грешка при избирането на изображенията - Моля, изчакайте… + Моля, изчакайте... напълно размазано Наблизо Прочетете повече diff --git a/app/src/main/res/values-blk/strings.xml b/app/src/main/res/values-blk/strings.xml index 51a8ec1ba8..2cf4aba5bd 100644 --- a/app/src/main/res/values-blk/strings.xml +++ b/app/src/main/res/values-blk/strings.xml @@ -38,7 +38,7 @@ အွောန်ႏဖေင်ꩻထိုꩻ ငဝ်းဗိဉ်ႏပလို့ꩻနဲ့? ဒင်ႏမတ်ပိုင်တိဉ် အဝ်ႏနွို့အကောက်ကျာꩻ - အိုင်ပွေားဆောင်းတဆင်ႏသြ… + အိုင်ပွေားဆောင်းတဆင်ႏသြ... နွို့အကောက်အောင်ႏလဲဉ်း! နွို့အကောက်အောင်ႏတဝ်း! မော့ꩻတဝ်းဖုဲင်၊ စံꩻထွားစံꩻသွော့ ဖုဲင်အလင်တဗာႏသြ။ @@ -97,7 +97,7 @@ မွေး! ထဲင်းယင်း သꩻတင်ꩻအချက်လက် ကဏ္ဍဖုံႏ - အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ… + အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ... လွိုက်ခါꩻတဝ်းမုဲင်ꩻမုဲင်ꩻ ပုင်ႏလိတ်အဝ်ႏတဝ်း အွောန်ႏနယ်ချက်အဝ်ႏတဝ်း diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 51502c2649..2d156c1999 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -393,7 +393,7 @@ কোনও চিত্র ব্যবহৃত হয়নি পঠিতগুলি দেখান অপঠিতগুলি দেখান - অনুগ্রহ করে অপেক্ষা করুন… + অনুগ্রহ করে অপেক্ষা করুন... অনুলিপি করা হয়েছে এই চিত্র এড়িয়ে যান প্রণেতা diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 9537c45e62..1c7d09617a 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -40,9 +40,6 @@ %1$d bellgargadenn loc\'het - %1$d bellgargadenn loc\'het - %1$d bellgargadennoù loc\'het - %1$d bellgargadennoù loc\'het %1$d pellgargadennoù loc\'het @@ -54,9 +51,6 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ - gant an aotre-implijout %1$s e vo an div skeudenn-mañ - gant an aotre-implijout %1$s e vo meur a skeudenn-mañ - gant an aotre-implijout %1$s e vo kalz a skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ Ergerzhout diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index d178ff507a..91860b1e12 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -10,22 +10,19 @@ Logo Commonsa postavlja se %1$d datoteka - postavlja se %1$d datoteke postavlja se %1$d datoteka + \@string/contributions_subtitle_zero postavljena %1$d datoteka - postavljena %1$d datoteke postavljenih datoteka: %1$d Započinjem postavljanje %1$d datoteke - Započinjem postavljanje %1$d datoteke Započinjem postavljanje %1$d datoteka/-e %1$d postavljanje - %1$d postavljanja %1$d postavljanja Slika će se voditi pod licencom %1$s diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 51330cb9d2..0c15e58c34 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,33 +20,27 @@ Imatge del dia s\'està carregant %1$d fitxer - S\'estan carregant de %1$d fitxers s\'estan carregant %1$d fitxers (%1$d) - (%1$d) (%1$d) S\'inicien les càrregues S\'està processant %1$d càrrega - S\'estan processant %1$d càrregues S\'estan processant %1$d càrregues %d càrrega - $d càrregues %d càrregues Aquesta imatge quedarà sota llicència %1$s - Aquestes imatges quedaran sota llicència %1$s Aquestes imatges quedaran sota llicència %1$s %1$d pujada - %1$d pujades %1$d pujades Explora @@ -398,7 +392,7 @@ Model de lent Números de sèrie Programari - Comparteix l\'aplicació a través de… + Comparteix l\'aplicació a través de... Informació de la imatge No s’ha trobat cap categoria No s\'han trobat representacions diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index e25b83e259..e13e8c040d 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -64,7 +64,7 @@ Викиларма Параметраш Викиларма чуйаккха - ДӀадоьдуш ду чуйаккхар… + ДӀадоьдуш ду чуйаккхар... Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо @@ -146,7 +146,7 @@ ЦӀе: Сиднейн операн театр ХӀаъ! Категореш - Чуйолуш… + Чуйолуш... ХӀума хаьржина йац Куьг доцуш Хаамаш бац @@ -297,7 +297,7 @@ Серийн лоьмар Программан кхачам Файл йолу меттиган тӀекхача бакъо ца ло - Йекъа программа, гӀоьнца… + Йекъа программа, гӀоьнца... Суьртан информаци Цхьа а категори ца карийна. Цхьа а хаам ца карийна. @@ -362,7 +362,7 @@ ДӀайаьккхина закладки йукъара Цхьа хӀума галдаьлла. Фонан сурт хӀотто аьтто ца баьлла Фонан сурт санна хӀоттайе - Фонан сурт дӀахӀоттош ду… + Фонан сурт дӀахӀоттош ду... Системин нисдаран гӀирс Бодане Сирла diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fb4ee05ef3..4d49ee6327 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,44 +32,31 @@ Obrázek dne %1$d soubor se nahrává - %1$d soubory se nahrávají - %1$d souborů se nahrává %1$d souborů se nahrává + \@string/contributions_subtitle_zero (%1$d) - (%1$d) - (%1$d) (%1$d) Spouští se nahrávání %1$d souboru - Spouští se nahrávání %1$d souborů - Spouští se nahrávání %1$d souborů Spouští se nahrávání %1$d souborů %1$d nahrávání - %1$d nahrávání - %1$d nahrávání %1$d nahrávání Tento obrázek bude zveřejněn pod licencí %1$s - Tyto obrázky budou zveřejněny pod licencí %1$s - Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s %1$d nahrání - %1$d nahrávání - %1$d nahrávání %1$d nahrání Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení - Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení - Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Objevit diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 7cdfb13825..623a48c8c2 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -29,7 +29,7 @@ Wlogùjë mie Wregistrëjë sã Logòwanié - Proszã żdac… + Proszã żdac... Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. @@ -78,7 +78,7 @@ Sélôj òpinijã (przez e-mail) Felënk wjinstalowónegò e-mailowégò klienta Slédno ùżëwóne kategòrëje - Żdanié na pierszą synchronizacëjã… + Żdanié na pierszą synchronizacëjã... Nie môsz jesz wladowónych òdjimków Próbùjë znowa Òprzestóń @@ -99,7 +99,7 @@ Przëmiôr wladënka: Jo! Kategòrëje - Wladënk… + Wladënk... Felënk nacéchòwaniô Felënk òpisënka Nieznónô licencëja diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 50df9b6d8c..8c4b4a652a 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -21,44 +21,25 @@ Popeth Llun y Dydd - %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho \@string/contributions_subtitle_zero (%1$d) - (%1$d) - (%1$d) - (%1$d) (%1$d) Cychwyn Uwchlwytho - Dechrau %1$d uwchlwythiad Cychwyn %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad Cychwyn uwchlwytho %1$d ffeil - %1$d uwchlwythiad %1$d uwchlwythiad - %1$d uwchlwythiad - %1$d uwchlwythiad - %1$d uwchlwythiad %1$d uwchlwythiad - Ni chaiff unrhyw ddelweddau eu trwyddedu dan %1$s Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s Caiff y delweddau hyn eu trwyddedu dan %1$s Archwilio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 80a82afb54..3b6822c47b 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,7 @@ Adgang til medieplacering nægtet Vi kan muligvis ikke automatisk indhente placeringsdata fra billeder, du uploader. Tilføj den passende placering for hvert billede, før du indsender Upload billeder til Wikimedia Commons direkte fra din telefon. Download Commons-appen nu: %1$s - Del app via… + Del app via... Billedoplysninger Ingen kategorier blev fundet Ingen afbildninger fundet @@ -642,9 +642,9 @@ Begrænset forbindelsestilstand Kvalitetsbilleder Kvalitetsbilleder er tegninger eller fotografier, der opfylder visse kvalitetsstandarder (som for det meste er af teknisk karakter) og er værdifulde for Wikimedia-projekter - Genoptager upload… - Sætter upload på pause… - Annullerer upload… + Genoptager upload... + Sætter upload på pause... + Annullerer upload... Annuller upload Du har aktiveret begrænset forbindelsestilstand. Alle uploads er sat på pause og genoptages, når du deaktiverer denne tilstand. Begrænset forbindelsestilstand aktiveret! @@ -784,7 +784,7 @@ Andet problem eller anden information (forklar venligst nedenfor). Din feedback bliver slået op på følgende wiki-side: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Er du sikker på, at du vil annullere alle uploads? - Annullerer alle uploads… + Annullerer alle uploads... Uploads Afventer Mislykkedes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4043040214..a6471c1fe9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -516,7 +516,7 @@ Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten Bitte warten … - Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. + Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. Über Orte in der Nähe hochgeladene Bilder sind die Bilder, die von entdeckten Orten auf der Karte hochgeladen wurden. Diese Funktion erlaubt es Autoren, eine Dankeschön-Benachrichtigung an Benutzer zu senden, die nützliche Bearbeitungen durchgeführt haben – durch die Benutzung eines kleinen Dankeschön-Links in der Versionsgeschichte oder Unterschiedsseite. Auf Folgemedien kopieren @@ -611,7 +611,7 @@ zu den Lesezeichen hinzugefügt Etwas ist schiefgelaufen. Das Hintergrundbild konnte nicht eingestellt werden Als Hintergrundbild festlegen - Hintergrundbild wird festgelegt. Bitte warten… + Hintergrundbild wird festgelegt. Bitte warten... Systemeinstellung Dunkel Hell diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index ebb3cfbe4e..840b0198dc 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -62,8 +62,8 @@ Parola, xo vira kerde? Qeyd be Kewno cı - Kerem kerên, bıpawên… - Kerem ke, bıpawe… + Kerem kerên, bıpawên... + Kerem ke, bıpawe... Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. @@ -93,7 +93,7 @@ Şınasnayış Bınnuşte Xırabiya kewten-network xeta - Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2–3 deqey ra tepeya reyna bıcerrebnên. + Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Kodê kamiya raştkerdışi dıfaktorın gani cı kewê. Nidekeweya de @@ -298,7 +298,7 @@ Pêhesnayışê toyê wendışi çıniyê Wendışi bıvêne Nêwendeyan bıvêne - Kerem kerên, bıpawên… + Kerem kerên, bıpawên... Nê resımi raviyarnê Nuştekar Heqa telifi diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e4b597fb12..e3675ef0a3 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Σύνδεση Ξεχάσατε τον κωδικό πρόσβασης σας; Εγγραφή - Γίνεται σύνδεση… + Γίνεται σύνδεση... Παρακαλούμε αναμείνετε… Ενημέρωση λεζάντων και περιγραφών Παρακαλούμε αναμείνετε… @@ -204,7 +204,7 @@ Ναι! Περισσότερες πληροφορίες Κατηγορίες - Φόρτωση σε εξέλιξη… + Φόρτωση σε εξέλιξη... Καμία επιλεγμένη Χωρίς λεζάντα Χωρίς περιγραφή @@ -521,7 +521,7 @@ Δεν επιτρέπεται η πρόσβαση στην τοποθεσία πολυμέσων Ενδέχεται να μην μπορούμε να λάβουμε αυτόματα δεδομένα τοποθεσίας από φωτογραφίες που ανεβάζετε. Προσθέστε την κατάλληλη τοποθεσία για κάθε εικόνα πριν την υποβολή Ανεβάστε φωτογραφίες στα Wikimedia Commons απευθείας από το τηλέφωνό σας. Κάντε λήψη της εφαρμογής Commons τώρα: %1$s - Κοινή χρήση εφαρμογής μέσω… + Κοινή χρήση εφαρμογής μέσω... Πληροφορίες Εικόνας Δεν βρέθηκαν Κατηγορίες Δεν βρέθηκαν απεικονίσεις @@ -798,7 +798,7 @@ Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω). Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a> Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις; - Ακύρωση όλων των μεταφορτώσεων… + Ακύρωση όλων των μεταφορτώσεων... Μεταφορτώσεις Σε εκκρεμότητα Απέτυχε diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 323c823b25..69673afbed 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -78,7 +78,7 @@ Ĉu pasvorto forgesita? Registriĝi Ensalutado - Bonvolu atendi… + Bonvolu atendi... Ĝisdatiganta subtekstojn kaj priskribojn Bonvolu atendi… Ensalutado sukcesis @@ -150,7 +150,7 @@ Sendi viajn komentojn (per retpoŝto) Neniu retpoŝtilo instalita Laste uzitaj kategorioj - Atendas la unuan Sinkronigado… + Atendas la unuan Sinkronigado... Vi ankoraŭ ne alŝutis fotojn. Reprovi Nuligi @@ -190,7 +190,7 @@ Jes! <u>Ekscii pli</u> Kategorioj - Ŝargado… + Ŝargado... Neniu elektita Neniu substeksto Sen priskribo @@ -482,7 +482,7 @@ Vidu legitajn Vidi nelegitojn Eraro okazis dum elektado de bildoj - Bonvolu atendi… + Bonvolu atendi... Elstaraj bildoj estas tiuj bildoj far tre spertaj fotografistoj kaj ilustristoj, kiujn la komunumo de Vikimedia Komunejo elektis kiel iujn de la plej alta kvalito en la retejo. Bildoj Alŝutitaj per Apudaj lokoj estas bildoj alŝutitaj per trovado de lokoj sur la mapo. Tiu funkcio ebligas sendi Dankantan sciigon al farinto de utila redakto – per malgranda dankiga ligilo ĉe la paĝo de historio aŭ diferenco. @@ -504,7 +504,7 @@ Aliro al loko de plurmediaĵo malakceptita Ni eble ne povos aŭtomate akiri pri-lokajn datumojn de bildoj, kiujn vi alŝutas. Bonvolu aldoni la taŭgan lokon por ĉiu bildo antaŭ ol sendi Alŝutu fotojn al Vikimedia Komunejo rekte de via telefono. Elŝutu la Komunejan aplikaĵon nun: %1$s - Diskonigi aplikaĵon per… + Diskonigi aplikaĵon per... Informo pri Bildo Neniu Kategorio troviĝis Neniu bildo-priskribo trovita @@ -636,9 +636,9 @@ Modo por limigita konekto Kvalitaj Bildoj Kvalitaj bildoj estas diagramoj aŭ fotoj kiuj kontentigas certajn normojn pri kvalito (kiuj estas plejparte teknikaj) kaj estas valoraj por Vikimediaj projektoj. - Rekomencante alŝuton… - Paŭzante alŝuton… - Nuligante alŝuton… + Rekomencante alŝuton... + Paŭzante alŝuton... + Nuligante alŝuton... Ĉesigi alŝutadon Vi aktivigis Modon por limigita konekto. Ĉiuj alŝutoj estas paŭzitaj kaj rekomencos post kiam vi malŝaltos ĉi modon. Modo por limigita konekto estas aktivigita. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2189534705..4e90f68641 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,7 +51,7 @@ * Vivaelcelta * Wizardeck --> - + Página de Facebook de Commons Código fuente de Commons en GitHub Logo de Commons @@ -75,38 +75,31 @@ Foto del día Cargando %1$d archivo - Cargando %1$d archivos Cargando %1$d archivos (%1$d) - (%1$d) (%1$d) Comenzando las subidas Procesando %d carga - Procesando %d cargas Procesando %d cargas %d carga - %1 cargas %1 cargas Esta imagen se publicará bajo la licencia %1$s - Estas imágenes se publicarán bajo la licencia %1$s Estas imágenes se publicarán bajo la licencia %1$s %1$d Subida - %1$d Subidas %1$d Subidas Recepción de contenido compartido. El procesamiento de la imagen puede tardar cierto tiempo, dependiendo del tamaño de la imagen y de tu dispositivo - Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Explorar @@ -342,7 +335,7 @@ Omitir tutorial Internet no disponible Error al recuperar las notificaciones - Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. + Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. No se encontró ninguna notificación Traducir Idiomas @@ -484,7 +477,7 @@ Permitir Descartar Por favor, activa el acceso a la ubicación desde Configuración y vuelva a intentarlo. \n\nNota: Es posible que la subida no tenga datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. - La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. + La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. La aplicación no registrará la ubicación junto con las tomas debido a la falta del permiso de la ubicación. La aplicación no registrará la ubicación junto con las tomas porque el GPS está apagado Utilizar el selector de fotografías basado en documentos @@ -512,8 +505,8 @@ ¿Está correctamente categorizado? ¿Está dentro de los objetivos del proyecto? ¿Quieres agradecer al colaborador? - Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. - Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado + Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. + Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado Tu apreciación animara a %1$s ¡Oh, esto ni siquiera esta categorizado! Esta imagen esta dentro de %1$s categorías. @@ -531,7 +524,7 @@ Compartir registros usando Ver leídas Ver no leidas - Ocurrió un error mientras se elegían imágenes + Ocurrió un error mientras se elegían imagenes Un momento… Las imágenes destacadas son creaciones de talentosos fotógrafos e ilustradores que la comunidad de Wikimedia Commons ha reconocido como las de mayor calidad del sitio. Las imágenes subidas vía Lugares Cercanos son las imágenes que han sido subidas al descubrir lugares en el mapa. @@ -554,7 +547,7 @@ Acceso a la ubicación del archivo multimedia denegado Es posible que no podamos obtener automáticamente los datos de ubicación de las imágenes que suba. Añada la ubicación adecuada a cada imagen antes de enviarla Sube fotos a Wikimedia Commons directamente desde tu celular. Descarga la aplicación de Commons ahora: %1$s - Compartir la aplicación vía… + Compartir la aplicación vía... Información de la imagen No se encontró ninguna categoría No se encontraron representaciones @@ -581,7 +574,6 @@ Éxito Se añade %1$s categoría. - Se añaden %1$s categorías. Se añaden %1$s categorías. No se pudieron añadir las categorías. @@ -590,7 +582,6 @@ Editar las descripciones %1$s Se añade la descripción. - Descripción %1$s se añadieron. Descripción %1$s se añadieron. No se pueden añadir descripciones. @@ -608,7 +599,7 @@ Las coordenadas de la imagen no están actualizadas. No se puede obtener descripciones. Editar descripciones y leyendas - Compartir imagen via + Compartir imagen via Todavía no has hecho ninguna contribución. %s Aún no ha realizado ninguna contribución Cuenta creada @@ -633,7 +624,7 @@ añadido a marcadores Algo salió mal. No se pudo establecer el fondo de pantalla Colocar como fondo de pantalla - Estableciendo el fondo de pantalla. Por favor espere… + Estableciendo el fondo de pantalla. Por favor espere... Seguir sistema Oscuro Claro @@ -691,9 +682,9 @@ Modo de conexión limitada Imágenes de calidad Las imágenes de calidad son diagramas o fotografías que cumplen determinados estándares de calidad (mayormente de carácter técnico) y que son valiosas para proyectos de Wikimedia - Reanudando carga… - Pausando carga… - Cancelando carga… + Reanudando carga... + Pausando carga... + Cancelando carga... Cancelar carga Has habilitado el modo de conexión limitada. Todas las cargas están pausadas y se reanudarán cuando deshabilites este modo. El modo de conexión limitada está encendido. @@ -820,8 +811,7 @@ Guardar archivo GPX %d imagen seleccionada - %d imágenes seleccionadas - %d imágenes seleccionadas + %d imagenes seleccionadas Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado. Nota sobre cargas múltiples @@ -829,7 +819,7 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. - Cancelando todas las subidas… + Cancelando todas las subidas... Subidas Pendiente Falló diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 3dd463b345..ff75cbc7f6 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: Naturako elementuak (loreak, animaliak, mendiak) Objektu erabilgarriak (bizikletak, tren geltokiak) - Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat…) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) Mesedez EZ igo: Autorretratuak edo zure lagunen argazkiak Internetetik jaitsitako irudiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 4cdd2b87a9..841160581f 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -83,7 +83,7 @@ رمز عبور خودتان را فراموش کرده‌اید؟ ثبت نام واردشدن - شکیبا باشید… + شکیبا باشید... ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. @@ -122,7 +122,7 @@ تغییرها بارگذاری جستجوی رده‌ها - جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، …) + جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، ...) ذخیره تازه کردن فهرست @@ -411,7 +411,7 @@ شما هیچ اعلان خوانده‌شده‌ای ندارید نمایش دیده‌شده مشاهده خوانده نشده ها - لطفاً صبر کنید… + لطفاً صبر کنید... نمونه تصاویری که برای بازگذاری مناسب نیستند از این تصویر صرف نظر کن مدیریت تگ‌های EXIF @@ -423,7 +423,7 @@ مدل لنز شماره سریال نرم‌افزار - اشتراک از طریق… + اشتراک از طریق... اطلاعات عکس هیچ رده‌ای یافت نشد بارگذاری لغو شد @@ -455,7 +455,7 @@ به بوکمارک‌ها افزوده شد مشکل به وجود آمد. به عنوان پس‌زمینه انتخاب نشد. انتخاب به عنوان پس‌زمینه - قرار دادن پس‌زمینه. لطفاً صبر کنید… + قرار دادن پس‌زمینه. لطفاً صبر کنید... سامانه را دنبال کنید تیره روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 26328a3e2c..312ebc84c5 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -80,7 +80,7 @@ Kirjaudutaan Odota… Päivitetään kuvatekstejä ja kuvauksia - Odota… + Odota... Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. @@ -481,7 +481,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s - Jaa sovellus… + Jaa sovellus... Kuvan tiedot Luokkia ei löytynyt Kuvauksia ei löytynyt @@ -546,7 +546,7 @@ Lisätty kirjanmerkkeihin Jotain meni väärin. Ei voitu asettaa taustakuvaksi. Aseta taustakuvaksi - Asetetaan taustakuvaksi. Odota… + Asetetaan taustakuvaksi. Odota... Käytä järjestelmän Tumma Vaalea @@ -594,8 +594,8 @@ Rajoitettu yhteistila pois päältä. Jonossa olevat lähetykset kopioidaan nyt. Rajoitettu yhteystila Laatukuvat - Jatketaan lähettämistä… - Keskeytetään lähetys… + Jatketaan lähettämistä... + Keskeytetään lähetys... Peruutetaan tallennusta… Peruuta tallennus Rajoitettu yhteystila on päällä. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 995e4041b3..ae4dfb9664 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -46,7 +46,7 @@ * Wladek92 * Y-M D --> - + Page Facebook de Commons Code source Github de Commons Logo de Commons @@ -70,38 +70,31 @@ Image du jour %1$d fichier en cours de téléversement - %1$d fichiers en cours de téléversement %1$d fichiers en cours de téléversement (%1$d) - (%1$d) (%1$d) Démarrage des téléversements %d téléversement en cours - %d téléversements en cours %d téléversements en cours %d téléversement - %d téléversements %d téléversements Cette image sera sous licence %1$s. - Ces images seront sous licence %1$s. Ces images seront sous licence %1$s. %1$d téléversement - %1$d téléversements %1$d téléversements - Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. - Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. + Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. Explorer @@ -120,9 +113,9 @@ Mot de passe oublié ? S’inscrire Connexion - Veuillez patienter… + Veuillez patienter... Mise à jour des légendes et des descriptions - Veuillez patienter… + Veuillez patienter... Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. @@ -192,7 +185,7 @@ Envoyer vos commentaires (par courriel) Aucun client de courriel installé Catégories récemment utilisées - En attente de première synchronisation… + En attente de première synchronisation... Vous n’avez encore téléchargé aucune photo. Réessayer Annuler @@ -232,7 +225,7 @@ Oui ! Davantage d’informations Catégories - Chargement en cours… + Chargement en cours... Aucune catégorie sélectionnée Aucune légende Aucune description @@ -528,7 +521,7 @@ Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images - Veuillez patienter… + Veuillez patienter... Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisies comme étant de la meilleure qualité pour le site. Les images téléversées par « Lieux à proximité » sont les images téléversées lors de la découverte de lieux sur la carte. Cette fonctionnalité permet aux contributeurs d’envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles ― en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff. @@ -550,7 +543,7 @@ Accès à l’emplacement du média refusé Nous ne pourrons pas obtenir automatiquement les données de localisation des images que vous téléchargez. Veuillez ajouter l’emplacement approprié pour chaque image avant de la soumettre. Téléversez des photos sur Wikimedia Commons directement depuis votre téléphone. Téléchargez l’application Commons maintenant : %1$s - Partager l’application via… + Partager l’application via... Informations sur l’image Aucune catégorie trouvée Aucun élément représenté trouvé @@ -577,7 +570,6 @@ Succès La catégorie %1$s est ajoutée. - Les catégories %1$s sont ajoutées. Les catégories %1$s sont ajoutées. Impossible d’ajouter des catégories. @@ -586,7 +578,6 @@ Modifier les éléments représentés L’élément représenté %1$s est ajouté. - Les éléments représentés %1$s sont ajoutés. Les éléments représentés %1$s sont ajoutés. Impossible d’ajouter des éléments représentés. @@ -629,7 +620,7 @@ Ajouté aux favoris Un problème est survenu. Impossible d’installer le fond d’écran. Définir comme fond d’écran - Installation du fond d’écran. Veuillez patienter… + Installation du fond d’écran. Veuillez patienter... Suivre le système Sombre Clair @@ -687,9 +678,9 @@ Mode de connexion limitée Images de qualité Les images de qualité sont des diagrammes ou des photographies qui respectent certains standards de qualité (qui sont, par nature, essentiellement techniques) et sont précieuses pour les projets Wikimedia. - Reprise du téléversement… - Mise en pause du téléversement… - Annulation du téléversement… + Reprise du téléversement... + Mise en pause du téléversement... + Annulation du téléversement... Annuler le téléversement Vous avez activé le mode de connexion limitée. Tous les téléversements sont suspendus et reprendront une fois ce mode désactivé. Le mode de connexion limitée est actif. @@ -818,7 +809,6 @@ Fichier GPX enregistré %d image sélectionnée - %d images sélectionnées %d images sélectionnées Souvenez-vous que toutes les images dans une importation multiple prennent les mêmes catégories et descriptions. Si les images de partagent pas les descriptions et catégories, veuillez effectuer plusieurs importations séparées. @@ -832,9 +822,12 @@ Autre problème ou information (merci d\'expliquer ci-dessous). Vos commentaires sont publiés sur la page wiki suivante : <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Êtes-vous sûr de vouloir annuler tous les téléchargements ? - Annulation de tous les téléchargements… + Annulation de tous les téléchargements... Téléversements En attente Échec Les données du lieu n\'ont pas pu être chargées + Cet endroit n\'a pas encore de photo, allez en prendre une ! + Cet endroit a déjà une photo. + Je vérifie maintenant si cet endroit a une photo. diff --git a/app/src/main/res/values-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index 4659eecf1a..b0ec664235 100644 --- a/app/src/main/res/values-gcr/strings.xml +++ b/app/src/main/res/values-gcr/strings.xml @@ -38,9 +38,9 @@ Ou bliyé ou Kodsigré ? Enskri oukò Konnègsyon - Souplé antann… + Souplé antann... Mizajou di léjann-yan ké dèskripsyon-yan - Souplé antann… + Souplé antann... Konnègsyon bon ! Konnègsyon pabon ! Fiché pa trouvé. Souplé éséyé ké rounòt. @@ -96,7 +96,7 @@ Enren ! Plis lenfòrmasyon Katégori-ya - Chajman ka fèt… + Chajman ka fèt... Pyès katégori sélègsyonnen Pyès léjann Pyès dèskripsyon diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index e11716a514..1740c1890c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -452,7 +452,7 @@ Modelo de lente Números de serie Software - Compartir a aplicación vía… + Compartir a aplicación vía... Información da imaxe Non se atoparon categorías Cancelouse a carga diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 2375838538..50a04319b9 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -39,6 +39,7 @@ %1$d फ़ाइलें अपलोड हो रहीं + \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -69,8 +70,8 @@ पासवर्ड भूल गये? खाता बनायें लॉग इन हो रहा है - कृपया प्रतीक्षा करें… - कृपया प्रतीक्षा करें… + कृपया प्रतीक्षा करें... + कृपया प्रतीक्षा करें... लॉग इन सफल! लॉग इन विफल! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। @@ -349,7 +350,7 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं… + सभी अपलोड रद्द किये जा रहे हैं... अपलोड लंबित विफल हुआ diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 414f0dd40a..d2d731c392 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -15,22 +15,19 @@ Slika dana Postavlja se %1$d datoteka - Postavlja se %1$d datoteke Postavljaju se %1$d datoteke + \@string/contributions_subtitle_zero %1$d postavljena datoteka - %1$d postavljena datoteke %1$d postavljene datoteke Započeto %1$d postavljanje - Započinjem %1$d postavljanja Započeta %1$d postavljanja %1$d postavljanje - %1$d postavljanja %1$d postavljanja Ova će slika biti licencirana pod %1$s @@ -49,7 +46,7 @@ Zaboravljena zaporka? Otvori račun Prijava - Molimo pričekajte … + Molimo pričekajte ... Prijava uspješna! Prijava neuspješna! Datoteka nije pronađena. Molimo probajte drugu. @@ -107,7 +104,7 @@ Pošaljite povratnu informaciju (putem elektroničke pošte) Klijent za elektroničku poštu nije instaliran Nedavno rabljene kategorije - Pričekajte za prvu sinkronizaciju… + Pričekajte za prvu sinkronizaciju... Nemate još postavljenih slika. Pokušaj ponovo Odustani @@ -147,7 +144,7 @@ Da! Više informacija Kategorije - Učitavanje… + Učitavanje... Ništa nije odabrano Nema opisa Nepoznata licencija @@ -196,7 +193,7 @@ Stranica datoteke na Zajedničkom poslužitelju Stavka na Wikidati Članak na Wikipediji - Opišite medij što je više moguće: gdje je napravljen, što prikazuje,… Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. Mogući problemi s ovom slikom: Slika je pretamna. Slika je mutna. @@ -284,7 +281,7 @@ Promijenio/la sam mišljenje, ne želim da više bude javno vidljivo Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće. Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo! - Molimo Vas, pričekajte … + Molimo Vas, pričekajte ... Preskoči ovu sliku Zadani jezik za opis Pokušavanje ažuriranja kategorija. @@ -296,7 +293,7 @@ Dodano u oznake Nešto je pošlo po zlu. Ne možemo postaviti pozadinu Postavi kao pozadinu - Postavljanje pozadine. Molimo, pričekajte… + Postavljanje pozadine. Molimo, pričekajte... Zadano Tamno Svijetlo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index eb34386749..aefc17d9d5 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -441,7 +441,7 @@ Sorozatszámok Szoftver Képek feltöltése Wikimedia Commons-ba közvetlenül a telefonodról. Töltsd le a Commons applikációt most: %1$s - Alkalmazás megosztása ezzel… + Alkalmazás megosztása ezzel... Képinformáció Nem található kategória Megszakított feltöltés @@ -474,7 +474,7 @@ Híd, múzeum, szálloda, stb. A belépés nem sikerült, kérj új jelszót. Beállítás háttérképnek - Beállítás háttérképnek. Kérem várjon… + Beállítás háttérképnek. Kérem várjon... Rendszerbeállítás követése Sötét Világos diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 8fff554e31..219fa45210 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,6 +61,7 @@ %1$d Unggahan + Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Jelajahi @@ -81,7 +82,7 @@ Memasuki log Silakan tunggu… Memperbarui takarir dan deskripsi - Mohon tunggu… + Mohon tunggu... Berhasil masuk log! Gagal masuk log! Berkas tidak ditemukan. Silakan coba berkas lain. @@ -190,7 +191,7 @@ Ya! Informasi selengkapnya Kategori - Memuat… + Memuat... Tidak ada yang dipilih Tanpa takarir Tidak ada keterangan @@ -496,7 +497,7 @@ Akses lokasi media ditolak Kami mungkin tidak dapat memperoleh data lokasi secara otomatis dari gambar yang Anda unggah. Harap tambahkan lokasi yang sesuai untuk setiap gambar sebelum mengirimkannya Mengunggah foto ke Wikimedia Commons secara langsung dari telepon Anda. Unduh aplikasi Commons sekarang: %1$s - Bagikan aplikasi lewat… + Bagikan aplikasi lewat... Info Gambar Kategori tidak ditemukan Penggambaran tidak ditemukan @@ -522,6 +523,7 @@ Pembaruan kategori Berhasil + Kategori %1$s ditambahkan. Kategori %1$s ditambahkan. Tidak bisa menambahkan kategori. @@ -567,7 +569,7 @@ Ditambahkan ke pembatas Terjadi kesalahan. Tidak bisa menetapkan wallpaper Jadikan Wallpaper - Sedang menetapkan Wallpaper. Tolong tunggu… + Sedang menetapkan Wallpaper. Tolong tunggu... Ikuti sistem Gelap Terang @@ -623,9 +625,9 @@ Mode Koneksi Terbatas Gambar Berkualitas Gambar berkualitas adalah diagram atau foto yang memenuhi standar kualitas tertentu (yang sifatnya teknis) dan berharga bagi proyek Wikimedia - Melanjutkan unggahan… - Menunda unggahan… - Membatalkan pengunggahan… + Melanjutkan unggahan... + Menunda unggahan... + Membatalkan pengunggahan... Batalkan pengunggahan Anda menyalakan mode koneksi terbatas. Semua pengunggahan ditunda dan akan dilanjutkan begitu Anda mematikan mode ini. Mode sambungan terbatas sedang menyala. @@ -741,7 +743,7 @@ %d gambar dipilih Bicara - Membatalkan semua unggahan… + Membatalkan semua unggahan... Unggahan Menunggu Gagal diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 994b1c3d34..51fe164419 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -70,9 +70,9 @@ Ka tu obliviis tua pasovorto? Enirar Eniranta - Voluntez vartar… + Voluntez vartar... Aktualiganta etiketi e deskripturi - Voluntez vartar… + Voluntez vartar... Eniro sucesoza! Eniro faliis! Arkivo ne trovita. Voluntez probar altr arkivo. @@ -142,7 +142,7 @@ Sendez komenti (per e-posto) Nula kliento di e-posto instalesis Kategorii recente uzita - Vartanta unesma sinkronigo… + Vartanta unesma sinkronigo... Vu ankore ne sendis fotografuri. Riprobar Nuligar @@ -180,7 +180,7 @@ Yes! Plusa informo Kategorii - Karganta… + Karganta... Nulo selektesis Nula deskripto-texto Nula deskripto @@ -410,7 +410,7 @@ Vu ne lektis irga avizo Vidar lektita Vidar ne-lektata - Vartez… + Vartez... Kopiita Exempli pri bona imaji por sendar a Commons Saltez ca imajo @@ -472,7 +472,7 @@ Ajusti Adjuntita marko-rubandi Uzar kom skreno-kovrilo - Kreanta skreno-kovrilo. Voluntez vartar… + Kreanta skreno-kovrilo. Voluntez vartar... Koloro obskura Koloro klara Charjez pluse @@ -500,7 +500,7 @@ Uzita Mea rango Imaji di qualeso - Nuliganta sendajo… + Nuliganta sendajo... Cesar kargajo Lektez pluse En omna idiomi diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ac64fbf2cd..4176529534 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ * Sveinki * Sveinn í Felli --> - + Commons Facebook-síðan Grunnkóði Commons á Github Táknmerki Commons @@ -51,7 +51,7 @@ %1$d innsendingar - Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns + Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndaanna og gerð tækisins þíns Uppgötva @@ -138,7 +138,7 @@ Senda umsögn (með tölvupósti) Ekkert tölvupóstforrit er uppsett Nýlega notaðir flokkar - Bíð eftir fyrstu samstillingu… + Bíð eftir fyrstu samstillingu... Þú ert ekki ennþá búin(n) að senda inn neinar myndir. Reyna aftur Hætta við @@ -477,7 +477,7 @@ Hugbúnaður Aðgangi að staðsetningu gagnamiðla hafnað Sendu myndir inn á Wikimedia Commons beint úr símanum þínum. Sæktu Commons-appið núna: %1$s - Deila forriti með… + Deila forriti með... Upplýsingar í mynd Engir flokkar fundust Engar myndlýsingar fundust diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e9aa8934ee..f408638708 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -46,38 +46,31 @@ Foto del giorno %1$d file in caricamento - %1$d file in caricamento %1$d file in caricamento (%1$d) - (%1$d) (%1$d) Avvio del caricamento Elaborando %d caricamento - Elaborando %d caricamenti Elaborando %d caricamenti %d caricamento - %d caricamenti %d caricamenti Questa immagine sarà rilasciata in base alla licenza %1$s - Queste immagini saranno rilasciate in base alla licenza %1$s Queste immagini saranno rilasciate in base alla licenza %1$s %1$d caricamento - %1$d caricamenti %1$d caricamenti Ricezione di contenuti condivisi. L\'elaborazione dell\'immagine potrebbe richiedere del tempo a seconda delle dimensioni dell\'immagine e del dispositivo - Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Esplora @@ -523,7 +516,7 @@ Accesso alla posizione multimediale negato Potremmo non essere in grado di ottenere automaticamente i dati sulla posizione dalle immagini caricate. Si prega di aggiungere la posizione appropriata per ciascuna immagine prima di inviarla Carica foto su Wikimedia Commons direttamente dal tuo telefono. Scarica subito l\'app Commons: %1$s - Condividi applicazione tramite… + Condividi applicazione tramite... Informazioni sull\'immagine Nessuna categoria trovata Nessuna definizione trovata @@ -550,7 +543,6 @@ Successo Categoria %1$s aggiunta. - Categorie %1$s aggiunte. Categorie %1$s aggiunte. Non è stato possibile aggiungere le categorie. @@ -583,7 +575,7 @@ Esiste Necessita della fotografia Tipo di luogo: - Ponte, museo, albergo, ecc… + Ponte, museo, albergo, ecc... Si è verificato un errore durante l\'accesso. Devi reimpostare la password! MEDIA CLASSI FIGLIE @@ -596,7 +588,7 @@ Aggiungi ai preferiti Qualcosa è andato storto. Non è stato possibile impostare lo sfondo schermo Imposta come sfondo - Impostazione di sfondo in corso… + Impostazione di sfondo in corso... Segui sistema Scuro Chiaro @@ -766,7 +758,6 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata - %d immagini selezionate %d immagini selezionate Questo posto non ha ancora una foto, scattane una! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 4b8c51f6c1..0b512102b4 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -45,37 +45,44 @@ מועלה קובץ אחד מועלים %1$d קבצים + מועלים %1$d קבצים מועלים %1$d קבצים (%1$d) (%1$d) + (%1$d) (%1$d) ההעלאות מתחילות עיבוד העלאה עיבוד d% העלאות + עיבוד d% העלאות עיבוד d% העלאות העלאה אחת %d העלאות + %d העלאות %d העלאות התמונה הזאת תפורסם ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s + התמונות האלה תפורסמנה ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s העלאה אחת %1$d העלאות + %1$d העלאות %1$d העלאות מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך + מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך לחקור @@ -94,9 +101,9 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין… + נא להמתין... עדכון כיתובים ותיאורים - נא להמתין… + נא להמתין... הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -206,7 +213,7 @@ כן! מידע נוסף קטגוריות - בטעינה… + בטעינה... לא נבחר דבר אין כיתוב אין תיאור @@ -501,7 +508,7 @@ הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין… + נא להמתין... תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -523,7 +530,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך… + שיתוף היישום דרך... פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -551,6 +558,7 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. + נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -560,6 +568,7 @@ נוסף מוצג %1$s נוספו המוצגים %1$s + נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -602,7 +611,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין… + הגדרת טפט. נא להמתין... מערכת מעקב כהה בהירה @@ -662,7 +671,7 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה… + ביטול ההעלאה... ביטול ההעלאה הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. @@ -792,6 +801,7 @@ נבחרה תמונה אחת נבחרו שתי תמונות + נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -805,7 +815,7 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות… + ביטול כל ההעלאות... העלאות ממתינות נכשלו diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f60bb30ddc..f20b986f82 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -44,6 +44,7 @@ %1$d 件のファイルをアップロード中 + (%1$d) (%1$d) アップロードを開始中です @@ -54,12 +55,14 @@ %d 件のアップロード + この画像は%1$sライセンスのもとにアップロードされます これらの画像は%1$sライセンスのもとにアップロードされます %1$d 件のアップロード + 共有コンテンツを受信中です。 この画像の投稿の処理には、サイズやご使用の機器により時間がかかる事があります 共有コンテンツの受信中です。投稿画像の処理には、サイズやご使用の機器により時間がかかる事があります 探索 @@ -557,7 +560,7 @@ ブックマークに追加 問題が発生しました。壁紙を設定できませんでした。 壁紙として設定 - 壁紙を設定中。お待ちください… + 壁紙を設定中。お待ちください... システムのまま ダーク ライト diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index eb90e4a23c..40eb016295 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -47,8 +47,8 @@ Qqen Tettuḍ awal uffir? Jerred - Tuqqna… - Rǧu… + Tuqqna... + Rǧu... Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -100,7 +100,7 @@ Azen tikti (s yimayl) Ulac amsaɣ n yimayl ibedden Taggayin yettwasqedcenmelmi kan - Araǧu n umtawi amezwaru… + Araǧu n umtawi amezwaru... Ur tsuliḍ ara yakan tiwlafin. Ɛref̣ tikelt-nniḍen Sefsex @@ -130,7 +130,7 @@ Tɣileḍ igarrez? Ih! Taggayin - Asali… + Asali... Ula d yiwet ur tettwafren Ulac aglam Turagt tarussint diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3703d373fb..aa7ae98e79 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,22 +43,28 @@ 검색 뷰 오늘의 이미지 + %1$d개의 파일을 올리는 중 %1$d개의 파일을 올리는 중 + (%1$d) (%1$d) 파일 올리기 + %1$d장의 업로드를 처리하는 중입니다 %1$d장의 업로드를 처리하는 중입니다 + %d개 업로드 %d개 업로드 + 이 그림은 %1$s에 따라 사용이 허가됩니다 이 그림은 %1$s에 따라 사용이 허가됩니다 + %1$d개 업로드 %1$d개 업로드 찾아보기 @@ -79,7 +85,7 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오… + 기다려 주십시오... 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -278,6 +284,7 @@ 위키텍스트를 클립보드에 복사했습니다 주변이 제대로 작동되지 않을 수 있습니다. 위치를 사용할 수 없습니다. 주변 장소의 목록을 표시하기 위한 권한이 필요합니다. + 주변 장소의 이미지 목록을 표시하기 위한 권한이 필요합니다 방향 위키데이터 위키백과 @@ -433,6 +440,7 @@ 완료 감사 표현 보내기: 성공 감사 표현 보내기: 실패 + 이것이 저작권 규정을 준수하고 있습니까? 알맞게 분류됐습니까? 기여자에게 감사를 표하시겠습니까? 앗, 분류가 달리지 않은 것 같습니다! @@ -447,10 +455,11 @@ 이미지가 올려지지 않음 읽지 않은 알림이 없습니다 읽은 알림이 없습니다 + 이메일의 받은 편지함을 확인하세요 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오… + 기다려 주십시오... 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -465,7 +474,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유… + 앱 공유... 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -493,6 +502,7 @@ 성공 설명이 추가되었습니다. 캡션이 추가되었습니다. + 좌표를 추가하지 못했습니다. 설명을 추가하지 못했습니다. 캡션을 추가하지 못했습니다. 이미지 좌표가 업데이트되지 않았습니다 @@ -523,7 +533,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오… + 배경화면을 설정 중입니다. 기다려 주십시오... 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 @@ -543,6 +553,7 @@ 일시 정지 계속하기 일시 중단됨 + 더 보기 책갈피 리더보드 순위: @@ -638,6 +649,9 @@ 전체 화면 선택 모드에 오신 것을 환영합니다 두 손가락으로 확대 / 축소하세요. 다음 방향으로 길고 재빠르게 넘겨보세요. \n- 왼쪽/오른쪽: 이전/다음으로 이동 \n- 위쪽: 선택\n- 아래쪽: 비업로드용으로 표시 + 스토리지 접근이 거부됨 + 이 항목을 공유할 수 없습니다 + 기능에 대한 권한이 필요합니다 유용한 설명을 추가하는 법 알아보기 유용한 캡션을 추가하는 법 알아보기 업적 보기 @@ -651,6 +665,7 @@ 작성자에게 감사 표시하기 작성자에게 감사를 표하던 도중에 오류가 발생하였습니다. 로그인 세션 만료. 다시 로그인해 주십시오. + GPX 파일을 열 수 있는 응용 프로그램이 없습니다 파일이 성공적으로 저장되었습니다 GPX 파일을 여시겠습니까? KML 파일을 여시겠습니까? @@ -658,8 +673,20 @@ GPX 파일을 저장하지 못했습니다. KML 파일을 저장 중 GPX 파일을 저장 중 + + %d개 이미지 선택됨 + 다중 업로드에 대한 참고사항 이 항목에 관한 문제를 위키데이터에 보고하기 + 의견을 입력해 주십시오 토론 기타 문제 또는 정보 (아래에 설명해 주십시오) + 모든 업로드를 취소하는 중... + 업로드 + 보류 중 + 실패 + 장소 데이터를 불러오지 못했습니다 + 이 장소에 아직 사진이 없습니다. 사진을 찍어보세요! + 이 장소에 이미 사진이 있습니다. + 지금 이 장소에 사진이 있는지 확인 중입니다. diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index be63e9db5b..55fa4ac359 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -143,7 +143,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады… + Биринчи синхронизацияны сакълаб турады... Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -227,7 +227,7 @@ Ызына ал Ач Джаб - Баш бет + Тамал бет Джюкле Джуўукъда Юсюнден @@ -498,7 +498,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле… + Къошакъны буну бла юлюшле... Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +575,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +633,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады… - Джюклениу туракъланады… - Джюклениу ызына алынады… + Джюклениу андан ары бардырылады... + Джюклениу туракъланады... + Джюклениу ызына алынады... Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index d9d5b65b91..506e9e4b47 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -70,8 +70,8 @@ Te şîfreya xwe ji bîr kir? Xwe tomar bike Têdikeve - Ji kerema xwe piçek bisekine … - Xêra xwe hinek bisekine… + Ji kerema xwe piçek bisekine ... + Xêra xwe hinek bisekine... Têketin bi ser ket! Têketin bi ser neket! Dosye nehat dîtin. Ji kerema xwe re dosyeyek din biceribîne. @@ -183,7 +183,7 @@ Wêneyên Barkirî Wêneyê din Belê, çima na - Ji kerema xwe piçek bisekine … + Ji kerema xwe piçek bisekine ... Wêne tevlî Wîkîpediyayê bike Tu dixwazî vê wêneyê tevlî gotara Wîkîpediyayê ya bi zimanê %1$s bikî? Pişrast bike diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index ab657b354a..8112afea61 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -49,7 +49,7 @@ Юклев уьлгю: Дюр! Категориялар - Юклев… + Юклев... Бир зат сайланмагъан Тасвири ёкъ Пикирлешивлер ёкъ diff --git a/app/src/main/res/values-kus/strings.xml b/app/src/main/res/values-kus/strings.xml index 02abd4ea10..99fb8c1f74 100644 --- a/app/src/main/res/values-kus/strings.xml +++ b/app/src/main/res/values-kus/strings.xml @@ -62,9 +62,9 @@ Fʋ tami fʋ paaswɛɛtɛ? Yɔ\'ɔgin kpɛn\' Kpɛn\'ɛdnɛ - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Maligim maal pian\'azut nɛ pa\'alʋg nam - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Kpɛn\'ɛb nyaŋya Kpɛn\'ɛb gʋ\'ʋŋya M Pʋ nyɛ faal la. M bɛlimnɛ tiakim faal si\'a. @@ -169,7 +169,7 @@ Ɛɛn! Labaya bɛdigʋ Buudi kɔn\'ɔb-kɔn\'ɔb - Bɛ tʋʋma ni… + Bɛ tʋʋma ni... Pʋ gaŋ si\'ela Pian\'azug kae Pa\'alʋg kae @@ -400,7 +400,7 @@ Gɔsim dinɛ ka fʋ karim sa Gɔsim dinɛ ka fʋ nam pʋ karim Daʋŋʋ kidig footonam la nɔkirin - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Footo banɛ ka fʋ kpɛn\'ɛsi dɔlis zin\'ibanɛ be yamma anɛ footo banɛ ka fʋ kpɛn\'ɛs ka di yinɛ fʋn nyɛ di map ni la. Yaam paas media banɛ bɛ tuon Yaaiya @@ -418,7 +418,7 @@ Serial Numbers Software Pʋ bas suor ye fʋ kpɛn\' midia zin\'iginɛ - Pʋdigim app la dɔlis… + Pʋdigim app la dɔlis... Footo labaar Pʋ paam buudinama Pʋ nyɛ nwɛnnɛm si\'aa. @@ -492,7 +492,7 @@ Ba zaŋi paas bookmarknamin Daʋŋsi\'a naam. Pʋ nyaŋi maal nibdaa footo la Maalimi fʋ nindaa footo la - Maanɛ nindaa footo. M bɛlimnɛ gu\'usim… + Maanɛ nindaa footo. M bɛlimnɛ gu\'usim... Dɔl sistɛm la Lik Nɛɛsim @@ -538,9 +538,9 @@ Bas suor ye di tʋm saŋa bi\'ela! Atʋm bi\'ela zi\'esim Footo sʋma - Lɛm pin\'in kpɛn\'ɛsʋg… - Gu\'om kpɛn\'ɛsʋg… - Basid kpɛn\'ɛsʋg… + Lɛm pin\'in kpɛn\'ɛsʋg... + Gu\'om kpɛn\'ɛsʋg... + Basid kpɛn\'ɛsʋg... Basim kpɛn\'ɛsʋg Bas suor ye di tʋm saŋa bi\'ela. Nwɛnnɛm nam diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 8b2ab6b955..2eb2fcf2f5 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -21,6 +21,7 @@ %1$d файл жүктөлүүдө + Азырынча жүктөөлөр жок 1 жүктөө %1$d жүктөө @@ -136,7 +137,7 @@ Жүктөөнү жокко чыгаруу Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз Жүктөөнү улантуу - Күтө туруңуз… + Күтө туруңуз... Аталыш Сыпаттама Элементтер diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index d99e269ab1..2ef8f9e031 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -61,7 +61,7 @@ Aloggen Waart wgl. … Beschrëftungen a Beschreiwungen aktualiséieren - Waart wgl. … + Waart wgl. ... Umeldung huet geklappt! D\'Aloggen huet net funktionéiert! Fichier net fonnt. Probéiert wgl. en anere Fichier. @@ -86,6 +86,7 @@ Nobäi Meng eropgeluede Fichieren Deelen + Fichierssäit weisen Beschrëftung (obligatoresch) Gitt wgl. eng Beschrëftung fir dëse Fichier un Beschreiwung @@ -93,6 +94,7 @@ Aloggen huet net funktionéiert – Problemer mam Reseau Ze dacks ouni Succès probéiert. Probéiert wgl. an e puer Minutten nach eng Kéier. Pardon, dëse Benotzer ass op Commons gespaart + Dir musst de Code vun Ärer Zwee-Facteur-Authentifizéierung uginn. Aloggen huet net funktionéiert Eroplueden Gitt dëser Biller een Numm @@ -349,7 +351,7 @@ Déi geliese weisen Déi net geliese weisen Feeler beim Eraussiche vun de Biller - Waart wgl. … + Waart wgl. ... Kopéiert Beispiller vu gudde Biller fir op Commons eropzelueden Beispiller fir Biller, déi een net eropluede sollt @@ -361,7 +363,7 @@ Seriennummeren Software Luet Fotoen direkt vun Ärem Handy op Wikimedia Commons erop. Luet d\'Commons-App elo erof: %1$s - App deelen iwwer… + App deelen iwwer... Bildinformatiounen Keng Kategorie fonnt. Eroplueden ofgebrach @@ -411,7 +413,7 @@ Bei d\'Lieszeechen derbäigesat Et ass Eppes schif gaangen. D\'Hannergrondbild konnt net agestallt ginn Als Hannergrondbild festleeën - Hannergrondbild gëtt agestallt. Waart wgl… + Hannergrondbild gëtt agestallt. Waart wgl... System suivéieren Däischter Hell @@ -454,7 +456,7 @@ Limitéierte Verbindungsmodus Qualitéitsbiller Qualitéitsbiller sinn Diagrammen oder Fotoen, déi gewësse Qualitéitscritèren erfëllen (déi haaptsächlech vun technescher Natur sinn) a wäertvoll fir Wikimedia-Projete sinn. - Eropluede gëtt ofgebrach…. + Eropluede gëtt ofgebrach.... Eroplueden ofbriechen Kategoriesäit weisen Sprooch vum Interface vum Benotzer vun der App diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index 1720bfbcb4..f477ed8f0b 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -33,8 +33,8 @@ Melj dich aan Wachwaord vergaete? Teiken dich in - Aan \'nt melje… - Wach estebleef… + Aan \'nt melje... + Wach estebleef... Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. @@ -88,7 +88,7 @@ Sjik feedback (mitten e-mail) Geine e-mailcliënt geïnstalleerd Recèntelik gebroekde categorieje - Oppe ieëste synchronisatie \'nt wachte… + Oppe ieëste synchronisatie \'nt wachte... Doe höbs nag gein plaetjes geüpload. Perbeer oppernuuj Braek aaf @@ -127,7 +127,7 @@ Versteis se \'t? Jao! Categorieje - \'nt laje… + \'nt laje... Geine gekaoze Gein besjrieving Ónbekande licentie diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cb7bebe41f..26a9bc7f77 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -33,27 +33,20 @@ Dienos nuotrauka %1$d keliamas failas - %1$d keliami failai - %1$d failų keliamas %1$d keliami failai - %1$d įkėlimas - %1$d įkėlimai - %1$d įkėlimų + \@string/contributions_subtitle_zero + 1 įkėlimas Įkėlimai pradedami Pradedamas %1$d įkėlimas - Pradedami %1$d įkėlimai - Pradedami %1$d įkėlimų Pradedami %1$d įkėlimai %1$d įkėlimas - %1$d įkėlimai - %1$d įkėlimų %1$d įkėlimai Šio paveikslėlio licencija bus %1$s @@ -75,7 +68,7 @@ Jungiamasi Prašome palaukti… Antraštės ir aprašymai atnaujinami - Prašome palaukti… + Prašome palaukti... Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. @@ -179,7 +172,7 @@ Taip! Daugiau informacijos Kategorijos - Kraunasi… + Kraunasi... Niekas nepasirinkta Nėra antraštės Nėra aprašymo @@ -472,7 +465,7 @@ Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida - Prašome palaukti… + Prašome palaukti... Rinktinės nuotraukos yra aukštos kvalifikacijos fotografų ir iliustratorių vaizdai, kuriuos Vikiteka bendruomenė pasirinko kaip svetainėje aukščiausios kokybės. Vaizdai, įkelti per Netoliese esančias vietas, yra vaizdai, kurie įkeliami atrandant vietas žemėlapyje. Ši funkcija leidžia redaktoriams siųsti padėkos pranešimą naudotojams, kurie atlieka naudingus pakeitimus, naudojant nedidelę padėkos nuorodą istorijos puslapyje arba skirtumų puslapyje. @@ -493,7 +486,7 @@ Prieiga prie medijos vietos uždrausta Gali būti, kad negalėsime automatiškai gauti vietos duomenų iš jūsų įkeltų nuotraukų. Prieš pateikdami kiekvienai nuotraukai pridėkite tinkamą vietą Įkelkite nuotraukas į Vikiteką tiesiai iš savo telefono. Atsisiųskite Vikitekos programėlę dabar: %1$s - Dalintis programą per … + Dalintis programą per ... Vaizdo informacija Kategorijų nerasta Vaizdų nerasta @@ -753,7 +746,7 @@ Kita problema arba informacija (paaiškinkite toliau). Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> Ar tikrai norite atšaukti visus įkėlimus? - Atšaukiami visi įkėlimai… + Atšaukiami visi įkėlimai... Įkėlimai Laukiama Nepavyko diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 9038eec9de..7a6d9e3628 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,7 @@ Reģistrēties Pieslēdzas Lūdzu, uzgaidiet… - Lūdzu, uzgaidi… + Lūdzu, uzgaidi... Ieiešana veiksmīga Pieteikšanās neizdevās. Autentifikācija neizdevās! @@ -163,7 +163,7 @@ Nākamais attēls Skatīt arhivētos Skatīt nelasītos - Lūdzu, uzgaidiet… + Lūdzu, uzgaidiet... Izlaist šo attēlu Autors Autortiesības diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index c496505ae9..916f4f4202 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4,7 +4,7 @@ * Violetova * Vlad5250 --> - + Ризницата на Фејсбук Изворен код на Ризницата на Github Лого на Ризницата @@ -52,7 +52,7 @@ %1$d подигања - Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред + Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликите и вашиот уред Истражи @@ -73,7 +73,7 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте… + Почекајте... Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -479,7 +479,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте… + Почекајте... Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +501,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку… + Сподели преку... Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -780,7 +780,7 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања… + Ги откажувам сите подигања... Подигања Во исчекување Неуспешно diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index 0d8e029a4c..de888dcbc3 100644 --- a/app/src/main/res/values-mni/strings.xml +++ b/app/src/main/res/values-mni/strings.xml @@ -18,7 +18,7 @@ ꯈꯨꯠꯌꯦꯛ ꯄꯤꯈꯠꯂꯨ ꯃꯅꯨꯡ ꯆꯪꯁꯤꯟꯂꯤ ꯉꯥꯏꯍꯥꯛ ꯉꯥꯏꯕꯤꯌꯨ - ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ… + ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ... ꯃꯥꯏꯄꯥꯛꯅꯥ ꯆꯪꯁꯤꯜꯂꯦ ꯫ ꯆꯪꯁꯤꯟꯕ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ! ꯐꯥꯏꯜ ꯊꯤꯕꯥ ꯐꯪꯗꯔꯦ ꯫ ꯆꯥꯟꯕꯤꯗꯨꯅꯥ ꯑꯇꯣꯞꯄ ꯑꯃꯥ ꯇꯧꯕꯤꯔꯣ ꯫ @@ -59,7 +59,7 @@ ꯍꯣꯏ! ꯑꯍꯦꯟꯕ ꯋꯥꯔꯣꯜ ꯃꯆꯥꯈꯥꯏꯕꯁꯤꯡ - ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ….. + ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ..... ꯑꯃꯠꯇ ꯈꯟꯗꯦ ꯑꯀꯨꯞꯄ ꯃꯔꯣꯜ ꯌꯥꯎꯗꯦ ꯈꯟꯅ-ꯅꯩꯅꯕ ꯂꯩꯇꯦ diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index 27a76b0a75..a6c18bca30 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -45,7 +45,7 @@ ဝိုတ်စ မအက္ခရ်ပၞုက် ပတိုန် စၟတ်သမ္တီ လုပ်လံက်အေန် ဒၟံင် - ပဂုန်တုဲ မင်မွဲလစုတ်… + ပဂုန်တုဲ မင်မွဲလစုတ်... လုက်အေန် အာစိုပ်ဒတုဲ! လံက်အေန် လီုလာ်! ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။ @@ -148,7 +148,7 @@ ယွံ! ဆက်လဴ ပရူတင်ဂၞင် ကဏ္ဍဂမၠိုင် - ပတိုန်ဒၟံင်… + ပတိုန်ဒၟံင်... ဟွံမဲကဵု ပရေၚ်ရုဲစှ် ဟွံမဲကဵု က္ဍိုပ်လိက် ဟွံမဲကဵု ဗမံက်ထ္ၜး diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 9655945855..546b43f4fa 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -17,6 +17,7 @@ %1$d संचिका अपभारीत होत आहे + अद्याप अपभारणे नाहीत एक अपभारण %1$d अपभारणे @@ -93,7 +94,7 @@ प्रतिसाद पाठवा (विपत्राद्वारे) कोणतेही ईमेल क्लायंट स्थापित नाहीत अलीकडे वापरलेले वर्ग - प्रथम संकालनाची प्रतीक्षा करीत आहे … + प्रथम संकालनाची प्रतीक्षा करीत आहे ... आपण अद्याप काहीच चित्रे अपभारीत केली नाहीत. पुन्हा प्रयत्न करा रद्द करा diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index e5dd0f3beb..1fce0c0daf 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -19,16 +19,20 @@ အားလုံး ယနေ့အတွက် အထူးဓာတ်ပုံ + ဖိုင် %1$d ခု တင်နေသည် ဖိုင် %1$d ခု တင်နေသည် အပ်ပလုဒ်များ စတင်ခြင်း + %1$d ခု တင်ထားသည် %1$d ခု တင်ထားသည် + ဤရုပ်ပုံသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် ဤရုပ်ပုံများသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် + %1$d အက်ပလုပ် %1$d အက်ပလုပ်များ ရှာဖွေစူးစမ်းပါ @@ -45,9 +49,9 @@ အကောင့်ဝင်ရန် စကားဝှက် မေ့နေပါသလား မှတ်ပုံတင်ရန် - လော့ဂ်အင် ဝင်ရောက်နေသည်… - ခေတ္တစောင့်ပါ… - ကျေးဇူးပြု၍ ခဏစောင့်ပါ… + လော့ဂ်အင် ဝင်ရောက်နေသည်... + ခေတ္တစောင့်ပါ... + ကျေးဇူးပြု၍ ခဏစောင့်ပါ... လော့အင် အောင်မြင်သည် လော့အင် မအောင်မြင်ပါ ဖိုင်မတွေ့ပါ၊ အခြးဖိုင်တစ်ခု စမ်းကြည့်ပါ။ @@ -129,7 +133,7 @@ ဟုတ်ကဲ့ သတင်းအချက်အလက် ပို၍ ကဏ္ဍများ - ဝန်ဆွဲတင်နေသည်… + ဝန်ဆွဲတင်နေသည်... ဘာမှရွေးချယ်မထားပါ ပုံစာ မရှိ ဖော်ပြချက် မရှိ @@ -311,7 +315,7 @@ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ရုပ်ပုံများကိုရွေးနေစဉ် အမှားဖြစ်ပွားခဲ့ပါသည် - ကျေးဇူးပြု၍ ခဏစောင့်ပါ… + ကျေးဇူးပြု၍ ခဏစောင့်ပါ... နမူနာရုပ်ပုံများ အက်ပလုပ်တင်ရန် မဟုတ်ပါ ဤရုပ်ပုံအား ကျော်သွားမည် ဒေါင်းလုဒ် မအောင်မြင်ပါ။ ပြင်ပသိုလှောင်မှုခွင့်ပြုချက်မရှိဘဲ ဖိုင်ဒေါင်းလုဒ်မဆွဲနိုင်ပါ။ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3b4bf30dc9..de59e28de0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -522,7 +522,7 @@ Toegang tot medialocatie geweigerd Het is mogelijk dat we niet automatisch locatiegegevens kunnen verkrijgen van foto\'s die u uploadt. Voeg de locatie bij elke foto toe voordat u die upload Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s - App delen via… + App delen via... Afbeeldingsinfo Geen categorieën gevonden Geen beschrijvingen gevonden @@ -599,7 +599,7 @@ Als bladwijzer toegevoegd Er is iets fout gegaan. Kan de achtergrond niet instellen Instellen als achtergrond - Wordt ingesteld als achtergrond. Een ogenblik geduld… + Wordt ingesteld als achtergrond. Een ogenblik geduld... Systeem volgen Donker Licht @@ -659,7 +659,7 @@ Kwaliteitsafbeeldingen zijn diagrammen of foto\'s die voldoen aan bepaalde kwaliteitsnormen (die meestal technisch van aard zijn) en waardevol zijn voor Wikimedia-projecten Uploaden hervatten… Uploaden onderbreken… - Uploaden wordt geannuleerd… + Uploaden wordt geannuleerd... Uploaden Annuleren U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. Beperkte verbindingsmodus is ingeschakeld. @@ -806,4 +806,7 @@ In behandeling Mislukt Plaatsgegevens konden niet geladen worden + Er is nog geen foto van deze plek, maak er eentje! + Er is al een foto van deze plek. + We controleren nu of er een foto van deze plek is. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 62e01d4d56..7e11ea03a3 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -51,9 +51,9 @@ ߌ ߓߘߊ߫ ߢߌ߬ߣߊ߬ ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊ߫؟ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߝߍ߬ߛߓߍߟߌ ߣߌ߫ ߞߊ߲߬ߛߓߍߟߌ ߟߊߞߎߘߦߊ ߦߴߌ ߘߐ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫߹ ߞߐߕߐ߮ ߡߊ߫ ߛߐ߬ߘߐ߲߬. ߘߏ߫ ߜߘߍ߫ ߡߊߝߍߣߍ߲߫ ߖߊ߰ߣߌ߲߫. @@ -69,7 +69,7 @@ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲߬ ߞߐ߯ߟߕߊ ߟߎ߬ - ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫… + ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫... ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫ %1$d%% ߓߘߊ߫ ߘߝߊ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ @@ -148,7 +148,7 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -408,7 +408,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +418,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +486,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +533,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index eab67e0766..c097898e9f 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -81,7 +81,7 @@ Mandar vòstres comentaris (per corrièl) Cap de client de corrièl pas installat Categorias utilizadas recentament - Espèra de primièra sincronizacion… + Espèra de primièra sincronizacion... Avètz pas encara telecargat cap de fòto. Tornar ensajar Anullar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index d0ee733960..8c64900a56 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -8,7 +8,7 @@ * Sony dandiwal * ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ --> - + ਕਾਮਨਜ਼ ਮਾਰਕਾ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ @@ -17,10 +17,11 @@ ਸਾਰੇ ਦਿਨ ਦੀ ਤਸਵੀਰ - ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ + ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ %1$d ਫ਼ਾਈਲਾਂ ਚੜ੍ਹਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ + \@string/contributions_subtitle_zero %1$d upload %1$d ਅੱਪਲੋਡ @@ -29,7 +30,7 @@ %1$d ਸ਼ੁਰੂ ਹੋ ਰਹੇ ਹਨ - %1$d ਅੱਪਲੋਡ + &d ਅੱਪਲੋਡ %1$d ਅੱਪਲੋਡਾਂ ਇਹ ਤਸਵੀਰ ਦਾ %1$s ਹੇਠ ਲਸੰਸ ਜਾਰੀ ਕੀਤੀ ਜਾਵੇਗਾ @@ -44,7 +45,7 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -128,7 +129,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ… + ਲੱਦ ਰਿਹਾ ਹੈ... ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -200,7 +201,7 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ ਲਿਖਤ ਛਾਪੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 09132f40e1..dcd8ea284a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -508,7 +508,7 @@ Zobacz przeczytane Wyświetl nieprzeczytane Wystąpił błąd podczas pobierania zdjęć - Proszę czekać… + Proszę czekać... Polecane zdjęcia to zdjęcia wysoko wykwalifikowanych fotografów i ilustratorów, które społeczność Wikimedia Commons wybrała jako jedne z najwyższych jakości na stronie. Obrazy przesłane przez Pobliskie miejsca to obrazy, które są przesyłane przez odkrywanie miejsc na mapie. Ta funkcja umożliwia redaktorom wysyłanie powiadomień z podziękowaniem do użytkowników, którzy dokonują przydatnych zmian - za pomocą małego linku z podziękowaniem na stronie historii lub na stronie diff. diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index b7449e9571..bfbd64413b 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -477,7 +477,7 @@ Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance - Ch\'a l\'abia passiensa… + Ch\'a l\'abia passiensa... Le fòto an evidensa a son ëd plance fàite da dij fotògraf e ilustrator motobin àbij che la comunità ëd Wikipedia Commons a l\'ha sernù tra cole ëd qualità pi àuta an sël sit. Le plance carià dai pòst ëd prossimità a son le plance carià con la dëscuverta dij pòst an sla carta. Costa fonsionalità a përmet ai contributor ëd mandé na notìfica d\'aringrassiament a j\'utent ch\'a fan dle modìfiche ùtij - an dovrand na cita liura d\'aringrassiament an sla pàgina dla stòria o cola dle diferense. @@ -499,7 +499,7 @@ Acess a la locassion dël mojen arfudà I podoma pa oten-e an automàtich ij dàit ëd localisassion dle plance che chiel a caria. Për piasì, ch\'a giontà la posission apropià për tute le plance prima ëd mandeje Ch\'a caria dle fòto su Wikimedia Commons diretaman da sò teléfon. Ch\'a dëscaria l\'aplicassion Commons adess: %1$s - Partagé l\'aplicassion via… + Partagé l\'aplicassion via... Anformassion an sla plancia Gnun-e categorìe trovà Gnun-e descrission trovà @@ -776,9 +776,12 @@ Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota). Ij sò sugeriment a saran giontà a coste pàgine wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> É-lo sigur ëd vorèj anulé tuti ij cariament? - Anulament ëd tuti ij cariament… + Anulament ëd tuti ij cariament... Cariament An atèisa Falì Impossìbil carié ij dàit dël pòst + Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! + Ës pòst a l\'ha già dle fòto. + An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 461cb6b1d8..4f17da26f7 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -65,7 +65,7 @@ CC BY 3.0 هو وېشنيزې - رابرسېرېږي… + رابرسېرېږي... هېڅ هم نه دی ټاکل شوی څرگندونه نشته نامعلوم جواز diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b0dd3b016a..3779a8a516 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,7 +28,7 @@ * Tuliouel * YuriNikolai --> - + Página do Commons no Facebook Código fonte do Commons no Github Logotipo do Commons @@ -51,39 +51,32 @@ Estado do local Imagem do Dia - carregando arquivo - carregando %1$d arquivos + carregando arquivo carregando %1$d arquivos (%1$d) - (%1$d) (%1$d) Iniciando carregamentos Processando %d carregamento - Processando %d carregamentos Processando %d carregamentos %d carregamento - %d carregamentos %d carregamentos Esta imagem será licenciada sob %1$s - Estas imagens serão licenciadas sob %1$s Estas imagens serão licenciadas sob %1$s %1$d carregamento - %1$d carregamentos %1$d carregamentos - Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo - Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo + Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Explorar @@ -102,9 +95,9 @@ Esqueceu a senha? Cadastre-se Efetuar login - Por favor, aguarde… + Por favor, aguarde... Atualizando legendas e descrições - Por favor, aguarde… + Por favor, aguarde... Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. @@ -168,7 +161,7 @@ Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"%1$s\">publicação no GitHub</a> para informar erros e sugestões. - Política de privacidade + Politica de privacidade Créditos Sobre Enviar comentários (por e-mail) @@ -255,7 +248,7 @@ Ponte de Arco-Íris Tulipa Bem-vindo à Wikipédia - Direitos de autor são bem-vindo + Direitos de autor são bem vindo Ópera de Sydney Cancelar Abrir @@ -313,7 +306,7 @@ Commons Avalie-nos Perguntas frequentes - Guia de usuário + Guia de usuario Pular Tutorial A Internet não está disponível Erro ao tentar obter as notificações @@ -528,7 +521,7 @@ Acesso à localização da mídia negado É possível que não possamos obter automaticamente os dados de localização das imagens que você carregar. Por favor adicione a localização adequada para cada imagem antes de envia-las Carregue fotos na wiki Wikimedia Commons, diretamente do seu celular. Baixe o aolicativo Commons agora: %1$s - Compartilhar aplicativo via… + Compartilhar aplicativo via... Informação da imagem Nenhuma categoria encontrada Nenhuma representação encontrada @@ -555,7 +548,6 @@ Sucesso A categoria %1$s foi adicionada. - As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -564,7 +556,6 @@ Editar representações O elemento retratado %1$s está adicionado. - Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -776,7 +767,6 @@ Salvar arquivo GPX %d imagem selecinada - %d imagens selecionadas %d imagens selecionadas Escreva algo sobre o item %1$s. Isso será visivel publicamente. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 19a52d72ff..bad9dc5005 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,7 @@ * Unamane * Vitorvicentevalente --> - + Página da wiki Commons no Facebook Código-fonte da wiki Commons no Github Logótipo da wiki Commons @@ -44,38 +44,31 @@ Imagem do Dia a carregar %1$d ficheiro - a carregar muitos %1$d ficheiros a carregar %1$d ficheiros (%1$d) - (%1$d) (%1$d) A iniciar carregamentos A processar %d carregamento - A processar %d carregamentos A processar %d carregamentos %d carregamento - %d carregamentos %d carregamentos Esta imagem será licenciada com a %1$s - Estas imagens serão licenciadas com a %1$s Estas imagens serão licenciadas com a %1$s %1$d carregamento - %1$d carregamentos %1$d carregamentos - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo A receber conteúdo partilhado. O processamento das imagens pode demorar algum tempo, dependendo do tamanho das mesmas e do seu dispositivo Explorar @@ -163,8 +156,8 @@ Política de privacidade Créditos Sobre - Enviar comentários (por correio eletrónico) - Não foi instalado nenhum cliente de correio eletrónico + Enviar comentários (por correio eletrónico) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. @@ -283,7 +276,7 @@ Gravar as fotografias tiradas com a câmara da aplicação no armazenamento do seu dispositivo Inicie sessão na sua conta Enviar ficheiro de registos - Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas + Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas Não foi encontrado nenhum navegador da Internet para abrir o URL Erro! Não foi possível encontrar o URL Nomear para eliminação @@ -498,7 +491,7 @@ Ver lidas Ver não lidas Ocorreu um erro ao escolher imagens - Aguarde, por favor… + Aguarde, por favor... As fotografias destacadas são imagens de fotógrafos e ilustradores altamente qualificados, que a comunidade da wiki Wikimedia Commons escolheu como as de melhor qualidade do \'\'site\'\'. As imagens carregadas via \"Locais próximos\" são as imagens que são carregadas descobrindo locais do mapa. Esta funcionalidade permite que os editores enviem uma notificação de agradecimento aos utilizadores que fizerem edições úteis - usando uma pequena hiperligação de agradecimento na página do historial ou na de diferenças. @@ -520,7 +513,7 @@ Acesso à localização de multimédia negado Podemos não conseguir obter automaticamente os dados de localização das fotografias que carregar. Adicione a localização apropriada de cada fotografia antes de a enviar, por favor Carregue fotografias na wiki Wikimedia Commons, diretamente do seu telemóvel. Descarregue a aplicação Commons agora: %1$s - Partilhar aplicação por… + Partilhar aplicação por... Informação da imagem Não foi encontrada nenhuma categoria Não foi encontrada nenhuma representação @@ -547,7 +540,6 @@ Êxito A categoria %1$s foi adicionada. - As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -556,7 +548,6 @@ Editar elementos retratados O elemento retratado %1$s está adicionado. - Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -598,7 +589,7 @@ Adicionado aos marcadores Ocorreu um problema. Não foi possível definir a imagem de fundo Definir como imagem de fundo - A definir a imagem de fundo. Aguarde, por favor… + A definir a imagem de fundo. Aguarde, por favor... Seguir sistema Escuro Claro @@ -654,8 +645,8 @@ Modo de ligação limitada Imagens de qualidade As imagens de qualidade são diagramas ou fotografias que satisfazem certos padrões de qualidade (principalmente de natureza técnica) e são valiosos para projetos da Wikimedia - A retomar carregamento… - A pausar carregamento… + A retomar carregamento... + A pausar carregamento... A cancelar o carregamento… Cancelar carregamento Ativou o modo de ligação limitada. Todos os carregamentos foram colocados em pausa e serão retomados quando desativar este modo. @@ -718,7 +709,7 @@ Não foi encontrada nenhuma localização Que tal adicionar o local onde a imagem foi tirada?\nOs dados de localização ajudam os editores da wiki a encontrarem a sua fotografia, tornando-a muito mais útil.\nObrigado! Adicionar localização - Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. + Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. Detalhes As realizações só estão disponíveis na versão de produção; consulte a documentação para programadores, por favor. A tabela de classificação só está disponível na versão prod. Consulte a documentação do desenvolvedor. @@ -769,7 +760,6 @@ Erro no envio de agradecimento ao autor. %d imagem selecionada - %d imagens selecionadas %d imagens selecionadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ad1d0b805f..0bcbc15506 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,8 @@ %1$d de fișiere se încarcă + \@string/contributions_subtitle_zero (%1$d) - (%1$d) (%1$d) Pornirea încărcărilor @@ -74,8 +74,8 @@ V-ați uitat parola? Înregistrare Se conectează - Vă rugăm să așteptați … - Vă rugăm să așteptați … + Vă rugăm să așteptați ... + Vă rugăm să așteptați ... Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. @@ -458,7 +458,7 @@ Vezi citit Vezi necitit A apărut o eroare la alegerea imaginilor - Vă rugăm să așteptați … + Vă rugăm să așteptați ... Imaginile de Calitate sunt imagini ale unor fotografi și ilustratori de înaltă calificare, pe care comunitatea Wikimedia Commons a ales-o ca fiind de cea mai înaltă calitate pe site. Imaginile Încărcate prin Locurile din Apropiere sunt imaginile care sunt încărcate prin descoperirea locurilor de pe hartă. Această caracteristică permite editorilor să trimită o notificare de Mulțumire utilizatorilor care fac modificări utile - folosind un mic link de mulțumire pe pagina istoric sau pe pagina dif. @@ -478,7 +478,7 @@ Numere Serie Software Încărcați fotografii pe Wikimedia Commons direct de pe telefon. Descărcați aplicația Commons acum: %1$s - Partajează aplicația prin … + Partajează aplicația prin ... Informații despre imagine Nu s-au găsit categorii Nu s-au Găsit Reprezentări diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 861d7ee27c..b15d777876 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ * ЛингвоЧел * ОйЛ --> - + Facebook-страница Викисклада Исходный код Викисклада на гитхабе Логотип Викисклада @@ -105,7 +105,7 @@ %1$d загрузок - Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства + Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства @@ -556,7 +556,7 @@ Отказано в доступе к местоположению файла Возможно, мы не сможем автоматически получать данные о местоположении из загруженных вами изображений. Пожалуйста, добавьте подходящее место для каждого изображения перед отправкой Загружайте фото на Викисклад прямо с телефона. Скачайте приложение Wikimedia Commons прямо сейчас: %1$s - Поделиться приложением с помощью… + Поделиться приложением с помощью... Информация об изображении Категории не найдены. Описания не найдены @@ -637,7 +637,7 @@ Добавлено в закладки Что-то пошло не так. Не удалось установить фоновую заставку Сделать фоновой заставкой - Идёт установка фоновой заставки… + Идёт установка фоновой заставки... Настройки системы Тёмная Светлая @@ -695,8 +695,8 @@ Режим ограниченного подключения Качественные изображения Качественные изображения - это диаграммы или фотографии, которые соответствуют определенным стандартам качества (которые в основном носят технический характер) и представляют ценность для проектов Викимедиа - Возобновление загрузки… - Приостановка загрузки… + Возобновление загрузки... + Приостановка загрузки... Отмена загрузки… Отменить загрузку Вы включили ограниченный режим подключения. Все загрузки приостановлены и возобновятся после отключения этого режима. @@ -841,7 +841,7 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок… + Отмена всех загрузок... Загрузки В ожидании Не удалось diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index d4b5916593..08f9a1fecc 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -169,7 +169,7 @@ ھا! وڌيڪ معلومات زمرا - لاهيندي… + لاهيندي... ڪوبہ چونڊيل ناھي عنوان ناهي ڪا تشريح ناھي @@ -315,7 +315,7 @@ لينس ماڊل سيريل انگ سافٽويئر - ايپ ذريعي ونڊيو… + ايپ ذريعي ونڊيو... عڪس معلومات زمرا نہ لڌا رد-ڪيل چاڙھ diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index 0489c363a1..78114e3362 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -44,9 +44,9 @@ Vajáldahttetgo beassansáni? Searvva Čáliha sisa - Vuordil… + Vuordil... Ođasmáhttá govvateavsttaid ja govvádusaid - Vuordil… + Vuordil... Sisačáliheapmi lihkostuvai! Sisačáliheapmi ii lihkostuvvan! Fiila ii gávdnon. Geahččal áinnas eará fiilla. @@ -112,7 +112,7 @@ Atte máhcahaga (e-poasttain) Ii leat ásahuvvon epoastadoaimmaheaddji Áitto geavahuvvon kategoriijat - Vuordime vuosttaš synkroniserema… + Vuordime vuosttaš synkroniserema... It leat vel bajásluđen ovttage gova. Geahččal ođđasit Gaskkalduhte @@ -143,7 +143,7 @@ Jua! Lassedieđut Kategoriijat - Luđeme… + Luđeme... Ii guhtege válljejuvvon Ii leat govvateaksta Ii gávdno govvádus diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 8e9cde75c9..5997677efd 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -112,7 +112,7 @@ Pošaljite Vašu povratnu informaciju (putem e-pošte) Nemate uspostavljen klijent za e-poštu Nedavno korištene kategorije - Čekam prvo usklađivanje… + Čekam prvo usklađivanje... Još uvijek niste otpremili nijednu sliku. Pokušaj ponovo Otkaži @@ -147,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje… + Učitavanje... Ništa nije odabrano Nema opisa Nema razgovora diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 0e661acb74..92fa25f3eb 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -5,24 +5,25 @@ * Sandaru * හරිත --> - + කොමන්ස් ෆේස්බුක් පිටුව කොමන්ස් ලාන්චනය කොමන්ස් වෙබ් අඩවිය - 1 ගොනුවක් උඩුගත කෙරේ + 1 ගොනුවක් උඩුගත කෙරේ ගොනු %d ක් උඩුගත කෙරේ - එක් උඩුගත කිරීමක් ඇත + තවමත් කිසිදු උඩුගත කිරීමක් නැත + එක් උඩුගත කිරීමක් ඇත උඩුගත කිරීම් %1$d ක් ඇත - 1 උඩුගත කිරීමක් ආරම්භ කරමින් + 1 උඩුගත කිරීමක් ආරම්භ කරමින් උඩුගත කිරීම් %1$d ක් ආරම්භ කරමින් - 1 උඩුගත කිරීමක් + 1 උඩුගත කිරීමක් උඩුගත කිරීම් %1$d ක් මෙම පින්තූරය %1$s යටතේ වලංගු වනු ඇත diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 99a0bf5483..49fc88a3b0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -491,7 +491,7 @@ Zobraziť prečítané Zobraziť neprečítané Nastala chyba pri vyberaní obrázkov - Čakajte, prosím… + Čakajte, prosím... Najlepšie obrázky sú fotografie od vysoko skúsených fotografov a ilustrátorov, ktoré vybrala komunita Wikimedie Commons ako jedny z najkvalitnejších na stránke. Obrázky nahrané cez Miesta v okolí sú obrázky, ktoré sú nahrané vďaka objavovaniu miest na mape. Táto funkcia umožňuje poslať poďakovanie za užitočné úpravy používateľom – použitím malého odkazu poďakovať v histórií stránky alebo na stránke rozdielu medzi revíziami. @@ -513,7 +513,7 @@ Prístup k polohe médií bol odmietnutý Možno nebudeme môcť automaticky získať údaje o polohe z obrázkov, ktoré nahráte. Pred odoslaním, prosím, pridajte ku každému obrázku údaj o polohe. Nahrávajte fotky na Wikimedia Commons priamo z vášho mobilu. Stiahnite si aplikáciu Wikimedia Commons teraz: %1$s - Zdieľať aplikáciu cez… + Zdieľať aplikáciu cez... Informácie o obrázku Nenájdené žiadne kategórie Neboli nájdené spôsoby vykreslovania @@ -593,7 +593,7 @@ Pridané do záložiek Niečo sa pokazilo. Tapetu sa nepodarilo nastaviť Nastaviť ako tapetu - Nastavujem tapetu. Prosím, čakajte… + Nastavujem tapetu. Prosím, čakajte... Predvolený systém Tmavý Svetlý @@ -651,9 +651,9 @@ Mód limitovaného pripojenia Kvalitné obrázky Kvalitné obrázky sú diagramy a fotografie, ktoré spĺňajú určité štandardy (ktoré sú väčšinou technického charakteru) a sú cenné pre projekty Wikimédie - Pokračovanie nahrávania… - Pozastavovanie nahrávania… - Prerušovanie nahrávania… + Pokračovanie nahrávania... + Pozastavovanie nahrávania... + Prerušovanie nahrávania... Zrušiť nahrávanie Zapli ste mód limitovaného pripojenia. Všetky nahrávania budú teraz pozastavené a budú pokračovať až po vypnutí tohto módu. Mód limitovaného pripojenia je zapnutý. @@ -787,7 +787,7 @@ Iný problém alebo informácia (vysvetlite nižšie). Vaša spätná väzba sa zverejní na nasledujúcej wiki stránke: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Ste si istí, že chcete zrušiť všetky nahrávania? - Ruším všetky nahrávania… + Ruším všetky nahrávania... Nahrané súbory Čakajúce Zlyhané diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index b91c3c0b13..61531980f1 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ * McDutchie * Upwinxp --> - + Facebook stran Zbirke Izvorna koda Zbirke v shrambi Github Logotip Zbirke @@ -66,8 +66,8 @@ %1$d nalaganj - Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. - Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. @@ -87,9 +87,9 @@ Ste pozabili geslo? Ustvari račun Prijavljanje - Prosimo, počakajte … + Prosimo, počakajte ... Posodabljam napise in opise - Prosimo, počakajte … + Prosimo, počakajte ... Uspešno ste se prijavili! Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. @@ -133,7 +133,7 @@ Spremembe Naloži Poišči kategorije - Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, …) + Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) Shrani Osveži Seznam @@ -159,7 +159,7 @@ Pošljite povratno informacijo (prek e-pošte) Nameščen ni noben e-poštni odjemalec Pred kratkim uporabljene kategorije - Čakam na prvo sinhronizacijo … + Čakam na prvo sinhronizacijo ... Naložili niste še nobene fotografije. Poskusi znova Prekliči @@ -199,7 +199,7 @@ Da! Več informacij Kategorije - Nalaganje … + Nalaganje ... Nič ni izbrano Ni napisa Ni opisa @@ -491,7 +491,7 @@ Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake - Prosimo, počakajte … + Prosimo, počakajte ... Izbrane slike so slike izvrstnih fotografov in ilustratorjev, ki jih je skupnost Wikimedijine zbirke prepoznala kot najbolj kakovostne v tem projektu. Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. @@ -513,7 +513,7 @@ Dostop do lokacije predstavnosti zavrnjen Za slike, ki jih nalagate, ne moremo samodejno pridobiti lokacije. Pred pošiljanjem dodajte za vsako sliko ustrezno lokacijo. Nalagajte slike v Wikimedijino zbirko neposredno iz telefona. Prenesite aplikacijo Commons zdaj: %1$s - Deli aplikacijo prek … + Deli aplikacijo prek ... Informacije o sliki Ni najdenih kategorij Ni najdenih upodobitev @@ -569,7 +569,7 @@ Koordinat ni bilo mogoče pridobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise - Deli slike prek … + Deli slike prek ... Ničesar še niste prispevali %s ni opravil_a še nobenega prispevka Račun ustvarjen! @@ -593,7 +593,7 @@ Dodano med zaznamke Nekaj je šlo narobe. Ozadja ni bilo mogoče nastaviti. Nastavi kot ozadje - Nastavljam ozadje. Prosimo, počakajte … + Nastavljam ozadje. Prosimo, počakajte ... Sledi sistemu Temna Svetla @@ -649,9 +649,9 @@ Način omejene povezanosti Kakovostne slike Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie - Nalaganje se nadaljuje… - Zaustavljam nalaganje… - Preklicujem nalaganje… + Nalaganje se nadaljuje ... + Zaustavljam nalaganje ... + Preklicujem nalaganje ... Preklic nalaganja Vklopili ste način omejene povezanosti. Vsa nalaganja so začasno ustavljena in se bodo nadaljevala, ko boste ta način izklopili. Način omejene povezanosti je vklopljen. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index f1e7412d48..40e99618fe 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -34,39 +34,32 @@ Слика дана %1$d датотека се отпрема - %1$d датотеке се отпремају %1$d датотеке се отпремају %1$d отпремање - %1$d отпремања %1$d отпремања Покретање отпремања Процесуирање %d отпремање - Процесуирање %d отпремања Процесуирање %d отпремања %d отпремање - %1$d отпремања %d отпремања Слика ће се водити под лиценцом %1$s - Слике ће се водити под лиценцом %1$s Слике ће се водити под лиценцом %1$s %1$d отпремање - %1$d отпремања %1$d отпремања - Пријем %d дељеног садржаја… Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја - Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја - Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Примање дељеног садржаја... Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја + Примање дељеног садржаја... Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја Истрага Изглед @@ -503,7 +496,7 @@ Приступ локацији медија је одбијен Можда нећемо моћи да аутоматски прибавимо податке о локацији из слика које отпремите. Додајте одговарајућу локацију за сваку слику пре објављивања Отпреми фотографије на Викимедијину Оставу директно са свог телефона. Преузми апликацију Оставе сада: %1$s - Подели апликацију преко… + Подели апликацију преко... Информације о слици Нису пронађене категорије Отказано отпремање @@ -528,13 +521,12 @@ Успешно Категорија %1$s је додата. - Категорије %1$s су додате. Категорије %1$s су додате. Није могуће додати категорије. Ажурирај категорију Уреди приказе - Ажурирање координата… + Ажурирање координата... Ажурирање координата Ажурирање описа Ажурирање натписа @@ -737,7 +729,6 @@ Чување GPX датотеке %d слика је одабрана - %d слике су одабране %d слика је одабрано Унесите коментар @@ -746,4 +737,6 @@ Отпремања На чекању Није успело + Ово место већ има слику + Проверавање да ли ово место има слику. diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 64379ac928..79ae5ea28f 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -26,25 +26,32 @@ Togel ka Luhur Gambar poé ieu + ngunjal %1$d berkas ngunjal %1$d berkas + (%1$d) (%1$d) Mitembeyan Ngamuat + Ngolah %d muatan Ngolah %d muatan + %1$d muatan %1$d muatan + Ieu gambar bakal dilisénsi %1$s Ieu gambar bakal dilisénsi %1$s + %1$d Dimuat %1$d Dimuat + Nampa kontén anu dibagikeun. Ngolah gambarna bisa jadi rada lila gumantung kana ukuran gambar jeung gaway anjeun Nampa kontén anu dibagikeun Langlang @@ -64,7 +71,7 @@ Asup log Tungguan… Nganyarkeun pertélaan jeung pedaran - Mangga tungguan… + Mangga tungguan... Laksana login! Gagal login! Berkas teu kapanggih. Coba berkas séjén. @@ -392,7 +399,7 @@ Tempo arsip Tempo nu can dibaca Éror pas keur nyomot gambar - Mangga tungguan… + Mangga tungguan... Iwalkeun ieu gambar Karya Hak cipta @@ -402,7 +409,7 @@ Nomer Seri Sopwér Muat poto ka Wikimedia Commons langsung tina ponsél. Unduh Commons App ayeuna: %1$s - Bagikeun app liwat… + Bagikeun app liwat... Info Gambar Euweuh Kategori kapanggih Muatan bedo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c49d64ad22..370bf0915f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -506,7 +506,7 @@ Åtkomst till mediaplats nekades Vi kanske inte automatiskt kan få platsdata från bilder du laddar upp. Lägg till lämplig plats för varje bild innan du skickar in Ladda upp foton till Wikimedia Commons direkt från din telefon. Ladda ned Commons-appen nu: %1$s - Dela appen via… + Dela appen via... Bildinfo Inga kategorier hittades Inga beskrivningar hittades @@ -783,7 +783,7 @@ Andra problem eller information (ange nedan). Din återkoppling kommer att skickas till följande wikisida: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a> Är du säker på att du vill avbryta alla uppladdningar? - Avbryter alla uppladdningar… + Avbryter alla uppladdningar... Uppladdningar Pågår Misslyckades diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4f41da5f6d..f9162bc7b4 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -98,7 +98,7 @@ பின்னூட்டம் அனுப்பு (மின்னஞ்சல் வழியாக) மின்னஞ்சற் செயலி எதுவும் நிறுவப்படவில்லை அண்மையிற் பயன்படுத்தப்பட்ட பகுப்புகள் - முதல் ஒத்திசைவுக்காக காத்திருக்கிறது … + முதல் ஒத்திசைவுக்காக காத்திருக்கிறது ... நீர் இன்னும் எவ்வொளிப்படத்தையும் பதிவேற்றவில்லை. மீண்டும் முயல்க கைவிடு @@ -131,7 +131,7 @@ ஆம்! மேலதிக தகவல்கள் பகுப்புகள் - ஏற்றப்படுகிறது… + ஏற்றப்படுகிறது... தெரிவு செய்யப்படவில்லை தலைப்பு இல்லை விளக்கம் இல்லை diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index 13ee985b95..add46f7b7b 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -110,7 +110,7 @@ ಇರೆನ ಅಬಿಪ್ರಾಯೊ ಬರೆಲೆ(ಮಿಂಚಂಚೆ). ಇರೆನ ಮಿಂಚಂಚೆ ಇಜ್ಜಿ. ಇಂಚಿಗ್ ಸೃಷ್ಟಿ ಮಾಲ್ತಿನ ವರ್ಗೊ. - ಒಂತೆ ಸಮಯ ಕಾಯೊಡು…. + ಒಂತೆ ಸಮಯ ಕಾಯೊಡು.... ಇರ್ ಒಂಜಿಲಾ ಪಟೋನ್ ಅಪ್ಲೋಡ್ ಮಾಲ್ತಿಜ್ಜಿ. ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ @@ -336,7 +336,7 @@ ಅನುರಕ್ಷಿತ ತೂಲೆ ಓದಂದಿನ ತೂಲೆ ಆಕೃತಿಲೆನ್ ಪೆಜ್ಜಿನಗ ದೋಷ ಆಂಡ್ - ದಯಮಲ್ತ್ ಕಾಪುಲೆ… + ದಯಮಲ್ತ್ ಕಾಪುಲೆ... ಸಂಯೋಜನೆಲು ಸೂಚನೆಲು ನನಾತ್ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 0c478b2214..ae80a53357 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -127,7 +127,7 @@ ఫీడుబ్యాకును పంపండి (ఈమెయిలు ద్వారా) ఈమెయిలు క్లయంటేదీ లేదు ఇటీవల వాడిన వర్గాలు - మొట్టమొదటి సింక్ కోసం చూస్తున్నాం… + మొట్టమొదటి సింక్ కోసం చూస్తున్నాం... ఇంకా మీరు ఫోటోలేమీ ఎక్కించలేదు. మళ్ళీ ప్రయత్నించు రద్దుచేయి @@ -457,7 +457,7 @@ క్రమ సంఖ్యలు సాఫ్టువేరు నేరుగా మీ ఫోను నుంచే వికీమీడియా కామన్స్‌కు ఫోటోలను ఎక్కించండి. కామన్స్ యాప్‌ను ఇప్పుడే దించుకోండి: %1$s - యాప్‌ను దీని ద్వారా పంచుకోండి… + యాప్‌ను దీని ద్వారా పంచుకోండి... బొమ్మ సమాచారం వర్గాలేమీ కనబడలేదు ఎక్కింపును రద్దు చేసాం @@ -523,7 +523,7 @@ బుక్‌మార్కులకు చేర్చాం ఏదో లోపం జరిగింది. వాల్‌పేపరును సెట్ చెయ్యలేకపోయాం వాల్‌పేపరుగా అమర్చు - వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి… + వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి... నల్లటి వెలుగుతో స్థానపు సెట్టింగులను తెరవడం విఫలమైంది. స్థానాన్ని మానవికంగా ఆన్ చెయ్యండి @@ -576,9 +576,9 @@ పరిమిత కనెక్షను మోడ్‌ను అచేతనం చేసాం. పెండింగులో ఉన్న ఎక్కింపులు తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ నాణ్యమైన బొమ్మలు - ఎక్కింపును తిరిగి మొదలెడుతున్నాం… - ఎక్కింపును నిలుపుతున్నాం… - ఎక్కింపును రద్దు చేస్తున్నాం… + ఎక్కింపును తిరిగి మొదలెడుతున్నాం... + ఎక్కింపును నిలుపుతున్నాం... + ఎక్కింపును రద్దు చేస్తున్నాం... ఎక్కింపును రద్దుచెయ్యి మీరు పరిమిత కనెక్షను మోడ్‌ను చేతనం చేసారు. ఎక్కింపులన్నీ నిలిచిపోయాయి. మీరు ఈ మోడ్‌ను అచేతనం చెయ్యగానే అవి తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ ఆన్ అయింది. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 70bee59ef5..125bba590f 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -38,16 +38,21 @@ รูปภาพประจำวัน กำลังอัปโหลดไฟล์ %1$d ไฟล์ + \@string/contributions_subtitle_zero + (%1$d) (%1$d) กำลังเริ่มอัปโหลด + กำลังเริ่มอัปโหลด %1$d รายการ กำลังเริ่มอัปโหลด %1$d รายการ + การอัปโหลด %1$d รายการ การอัปโหลด %1$d รายการ + ภาพนี้จะอยู่ในสัญญาอนุญาต %1$s ภาะเหล่านี้จะอยู่อยู่ในสัญญาอนุญาติ %1$s สำรวจ @@ -393,7 +398,7 @@ รุ่นเลนส์ หมายเลขซีเรียล ซอฟต์แวร์ - แบ่งปันแอปผ่าน… + แบ่งปันแอปผ่าน... ไม่พบหมวดหมู่ ภาพเซลฟี ภาพเบลอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 79482fb4e9..31a9f0b530 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -210,7 +210,7 @@ Evet! Daha Fazla Bilgi Kategoriler - Yükleniyor… + Yükleniyor... Hiçbir şey seçilmedi Altyazı yok Açıklama yok @@ -505,7 +505,7 @@ Okunanları görüntüle Okunmayanları görüntüle Resimler seçilirken hata oluştu - Lütfen bekleyin… + Lütfen bekleyin... Seçkin resimler, Wikimedia Commons topluluğunun sitedeki en yüksek kaliteden bazıları olarak seçtiği son derece yetenekli fotoğrafçıların ve illüstratörlerin görüntüleridir. Yakındaki yerler üzerinden yüklenen resimler, haritadaki yerleri keşfederek yüklenen resimlerdir. Bu özellik, editörlerin, geçmiş sayfasında veya fark sayfasında küçük bir teşekkür bağlantısı kullanarak faydalı düzenlemeler yapan kullanıcılara bir Teşekkür bildirimi göndermesine olanak tanır. @@ -604,7 +604,7 @@ Yer işaretlerine eklendi Bir şeyler yanlış gitti. Duvar kağıdı ayarlanamadı Duvar kağıdı olarak ayarla - Duvar Kağıdı ayarlanıyor. Lütfen bekleyin… + Duvar Kağıdı ayarlanıyor. Lütfen bekleyin... Sistemi izle Koyu Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cc2343a771..9e821ae24d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ * Ата * Пан Хаунд --> - + Facebook-сторінка Вікісховища Програмний код Вікісховища на GitHub Логотип Вікісховища @@ -81,7 +81,7 @@ %1$d завантажень - Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою + Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою @@ -612,7 +612,7 @@ Додано у закладки Щось трапилось. Не вдалося встановити шпалери робочого столу Встановити в якості шпалер робочого столу - Встановлення робочого столу. Будь ласка зачекайте… + Встановлення робочого столу. Будь ласка зачекайте... На взірець системи Темна Світла diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index f09f76ac62..a708c873a2 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -73,9 +73,9 @@ Parolni unutdingizmi? Roʻyxatdan oʻtish Kirish - Iltimos kuting… + Iltimos kuting... Sarlavhalar va tavsiflarni yangilash - Iltimos, kutib turing… + Iltimos, kutib turing... Kirish muvaffaqiyatli bajarildi! Kirish muvaffaqiyatsiz yakunlandi! Fayl topilmadi. Iltimos, boshqa faylni izlab koʻring. @@ -180,7 +180,7 @@ Ha! Batafsil maʼlumot Turkumlar - Yuklanmoqda… + Yuklanmoqda... Tanlanmagan Izoh yoʻq Tavsif yoʻq @@ -390,7 +390,7 @@ Xatchoʻplar Xatchoʻplar Bajarildi - Iltimos, kuting… + Iltimos, kuting... EXIF teglarni boshqarish Muallif Mualliflik huquqlari diff --git a/app/src/main/res/values-vec/strings.xml b/app/src/main/res/values-vec/strings.xml index 52bb495ea8..bbcb64561a 100644 --- a/app/src/main/res/values-vec/strings.xml +++ b/app/src/main/res/values-vec/strings.xml @@ -68,7 +68,7 @@ Cargamento de %1$s no riusio Schicia par vixuałixare I me ultimi cargamenti - In coa… + In coa... Fałimento %1$d%% conpleto Drio cargar.. @@ -114,7 +114,7 @@ Mandane on comento (co ła mail) Nisun client de posta eletronega instałà Categorie doparà ultimamente - Speta par ła prima sincronixasion… + Speta par ła prima sincronixasion... No te ghe njiancora cargà na foto Riproa Descançełare @@ -403,7 +403,7 @@ Varda no lexeste Varda no lexeste Se ga vuo on eror co se jera drio ełexare łe imajini. - Speta on fià… + Speta on fià... Le foto in primo pian łe xé imajini de fotografi altamente cuałifegai che ła comunità de Wikimedia Commons ła ga ełeto come fotografi de alta cuałità sol sito. Imajini cargae via \"Posti cuà rente\", imajini che łe njien cargae scoerxendo posti n\'te ła mapa Sta funsion ła consente ai editori de enviar na notifega de ringrasiamento ai uxuari che i fa modifeghe che serve, doparando on lingambo picenin de ringrasiamento n\'te ła pajina del storego o n\'te ła pajina de łe difarense.\n\nQuesta funzione consente agli editor di inviare una notifica di ringraziamento agli utenti che apportano modifiche utili, utilizzando un piccolo link di ringraziamento nella pagina della cronologia o nella pagina delle differenze. @@ -421,7 +421,7 @@ Numari seriałi Software Carga foto so Wikimedia Commons diretamente dal to tełefonin. Descarga l\'aplicasion deso: %1$s - Spartisi aplicasion co… + Spartisi aplicasion co... Informasion so l\'imajine Nisuna categoria catada Cargamento nułà @@ -461,7 +461,7 @@ Xonta ai favorii Calcosa el xé ndà roerso. No xé sta pusibiłe canbiar el sfondo Inposta el sfondo - Drio inpostar el sfondo. Speta on fià… + Drio inpostar el sfondo. Speta on fià... Segui el sistema Scuro Ciaro diff --git a/app/src/main/res/values-xal/strings.xml b/app/src/main/res/values-xal/strings.xml index c362060613..346ff15e17 100644 --- a/app/src/main/res/values-xal/strings.xml +++ b/app/src/main/res/values-xal/strings.xml @@ -23,15 +23,15 @@ Вики-аһулх һазр Тохрллһ Вики-аһулх һазрур ацалх - Ацалгдҗана… + Ацалгдҗана... Кергләчин нерн Нууц үг Невтрх Нууц үгән мартвт? Бүрткүлх Невтрҗәнә - Күләхнтн… - Күләхнтн… + Күләхнтн... + Күләхнтн... Невтрлт амҗлтта болла! Невтрҗ чадсн уга! Ацаллт кеҗ экллә! @@ -83,7 +83,7 @@ Тиим Делгрңгү Нерн, төрл - Умшҗана… + Умшҗана... Алькинь чигн суңһад уга Тодрхаллт уга Күүндән уга diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index 7927da1af5..b32eeb0057 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -61,7 +61,7 @@ ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა - ეთმიხარგუ… + ეთმიხარგუ... მახვარებუშ ჯოხო პაროლი გენშართით თქვანი პროფილით Commons Beta-შა @@ -71,7 +71,7 @@ სისტემაშა მიშულა ქორთხინთ ქჷმიცადით … მუკნაჭარეფი დო ეჭარუეფი მითმიახალებუ - ქორთხინთ ქჷმიცადით… + ქორთხინთ ქჷმიცადით... სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. @@ -181,7 +181,7 @@ ქოǃ უმოსი ინფორმაცია კატეგორიეფი - იხარგუ… + იხარგუ... მუთუნ ვა რე გიშაგორილი მუკნაჭარა ვა რე ვა რე ეჭარუა diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index c6f27bb991..27080b9991 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -13,7 +13,7 @@ ⵜⴻⵜⵜⵓⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵣⵔⴰⵢ? ⵣⵎⵎⴻⵎ ⴷⴰ ⵜⴽⵛⵛⵎⴷ - ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ… + ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ... ⴰⴽⵛⴰⵎ !ⵉⵎⵓⵔⵙ ⴰⴽⵛⴰⵎ ⵉⵣⴳⵍ! ⴰⴼⴰⵢⵍⵓ ⵓⵔ ⵉⵜⵜⵢⵓⴼⴰ. ⴰⵎⵓⵔ ⵏⵏⴽ ⴰⵔⵎ ⴰⴼⴰⵢⵍⵓ ⵢⴰⴹⵏ. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2a307e955c..2adabec26e 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,22 +79,28 @@ 地点状态 今日图片 + %1$d个文件正在上传 %1$d个文件正在上传 + %1$d次上传 %1$d次上传 开始上传 + 正在处理%d个上传 正在处理%d个上传 + %d个上传 %d个上传 + 该图像的授权协议是 %1$s 这些图像的授权协议是 %1$s + %1$d次上传 %1$d次上传 @@ -546,7 +552,7 @@ 已拒绝访问媒体位置 我们可能无法自动从你上传的图片中获取位置数据。提交前请为每张图片添加适当的位置 直接在您手机上的维基共享资源应用中上传照片。立即下载共享资源应用:%1$s - 分享到… + 分享到... 图像信息 找不到分类 找不到描写。 @@ -572,6 +578,7 @@ 分类更新 成功 + 分类%1$s已添加。 分类%1$s已添加。 无法添加分类。 @@ -579,6 +586,7 @@ 正在尝试更新描述。 编辑描述 + 已添加 %1$s 个描写。 已添加 %1$s 个描写。 无法添加描述。 @@ -679,8 +687,8 @@ 限制连接模式 优良图片 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值 - 正在恢复上传… - 暂停上传… + 正在恢复上传... + 暂停上传... 正在取消上传… 取消上传 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。 @@ -808,6 +816,7 @@ 正在保存KML文件 正在保存GPX文件 + 已选择%d个图像 已选择%d个图像 请记住,每次多图片上传会为其中的所有图片标注相同的分类和描述。如果这些图片并不共享同样的描述和分类,请分别进行多次上传。 @@ -821,9 +830,12 @@ 其他问题或信息(请在下方解释)。 您的反馈已经发布在以下wiki页面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> 您确定要取消所有上传吗? - 取消所有的上传… + 取消所有的上传... 上传 待处理 失败 无法加载地点数据 + 这个地点还没有照片,快去拍一张吧! + 这个地点已经有照片了。 + 现在检查这个地点是否有照片。 From 4a46488e4e64c0e75768e266652c775c7bec0546 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 28 Oct 2024 13:02:08 +0100 Subject: [PATCH 007/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ab/strings.xml | 4 +- app/src/main/res/values-af/strings.xml | 3 +- app/src/main/res/values-anp/strings.xml | 10 ++--- app/src/main/res/values-ar/strings.xml | 8 ++-- app/src/main/res/values-as/strings.xml | 4 +- app/src/main/res/values-ast/strings.xml | 4 +- app/src/main/res/values-az/strings.xml | 2 +- .../main/res/values-b+roa+tara/strings.xml | 6 +-- app/src/main/res/values-b+sr+Latn/strings.xml | 19 +++------ app/src/main/res/values-ba/strings.xml | 8 ++-- app/src/main/res/values-ban/strings.xml | 6 +-- app/src/main/res/values-bg/strings.xml | 2 +- app/src/main/res/values-blk/strings.xml | 4 +- app/src/main/res/values-bn/strings.xml | 2 +- app/src/main/res/values-br/strings.xml | 6 --- app/src/main/res/values-bs/strings.xml | 5 +-- app/src/main/res/values-ca/strings.xml | 8 +--- app/src/main/res/values-ce/strings.xml | 8 ++-- app/src/main/res/values-cs/strings.xml | 15 +------ app/src/main/res/values-csb/strings.xml | 6 +-- app/src/main/res/values-cy/strings.xml | 19 --------- app/src/main/res/values-da/strings.xml | 10 ++--- app/src/main/res/values-de/strings.xml | 4 +- app/src/main/res/values-diq/strings.xml | 8 ++-- app/src/main/res/values-el/strings.xml | 8 ++-- app/src/main/res/values-eo/strings.xml | 16 ++++---- app/src/main/res/values-es/strings.xml | 38 +++++++----------- app/src/main/res/values-eu/strings.xml | 2 +- app/src/main/res/values-fa/strings.xml | 10 ++--- app/src/main/res/values-fi/strings.xml | 10 ++--- app/src/main/res/values-fr/strings.xml | 39 ++++++++----------- app/src/main/res/values-gcr/strings.xml | 6 +-- app/src/main/res/values-gl/strings.xml | 2 +- app/src/main/res/values-hi/strings.xml | 7 ++-- app/src/main/res/values-hr/strings.xml | 17 ++++---- app/src/main/res/values-hu/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 18 +++++---- app/src/main/res/values-io/strings.xml | 14 +++---- app/src/main/res/values-is/strings.xml | 8 ++-- app/src/main/res/values-it/strings.xml | 15 ++----- app/src/main/res/values-iw/strings.xml | 26 +++++++++---- app/src/main/res/values-ja/strings.xml | 5 ++- app/src/main/res/values-kab/strings.xml | 8 ++-- app/src/main/res/values-ko/strings.xml | 35 +++++++++++++++-- app/src/main/res/values-krc/strings.xml | 14 +++---- app/src/main/res/values-ku/strings.xml | 6 +-- app/src/main/res/values-kum/strings.xml | 2 +- app/src/main/res/values-kus/strings.xml | 18 ++++----- app/src/main/res/values-ky/strings.xml | 3 +- app/src/main/res/values-lb/strings.xml | 12 +++--- app/src/main/res/values-li/strings.xml | 8 ++-- app/src/main/res/values-lt/strings.xml | 21 ++++------ app/src/main/res/values-lv/strings.xml | 4 +- app/src/main/res/values-mk/strings.xml | 12 +++--- app/src/main/res/values-mni/strings.xml | 4 +- app/src/main/res/values-mnw/strings.xml | 4 +- app/src/main/res/values-mr/strings.xml | 3 +- app/src/main/res/values-my/strings.xml | 14 ++++--- app/src/main/res/values-nl/strings.xml | 9 +++-- app/src/main/res/values-nqo/strings.xml | 20 +++++----- app/src/main/res/values-oc/strings.xml | 2 +- app/src/main/res/values-pa/strings.xml | 13 ++++--- app/src/main/res/values-pl/strings.xml | 2 +- app/src/main/res/values-pms/strings.xml | 9 +++-- app/src/main/res/values-ps/strings.xml | 2 +- app/src/main/res/values-pt-rBR/strings.xml | 28 +++++-------- app/src/main/res/values-pt/strings.xml | 32 ++++++--------- app/src/main/res/values-ro/strings.xml | 10 ++--- app/src/main/res/values-ru/strings.xml | 14 +++---- app/src/main/res/values-sd/strings.xml | 4 +- app/src/main/res/values-se/strings.xml | 8 ++-- app/src/main/res/values-sh/strings.xml | 4 +- app/src/main/res/values-si/strings.xml | 11 +++--- app/src/main/res/values-sk/strings.xml | 14 +++---- app/src/main/res/values-sl/strings.xml | 30 +++++++------- app/src/main/res/values-sr/strings.xml | 19 +++------ app/src/main/res/values-su/strings.xml | 13 +++++-- app/src/main/res/values-sv/strings.xml | 4 +- app/src/main/res/values-ta/strings.xml | 4 +- app/src/main/res/values-tcy/strings.xml | 4 +- app/src/main/res/values-te/strings.xml | 12 +++--- app/src/main/res/values-th/strings.xml | 7 +++- app/src/main/res/values-tr/strings.xml | 6 +-- app/src/main/res/values-uk/strings.xml | 6 +-- app/src/main/res/values-uz/strings.xml | 8 ++-- app/src/main/res/values-vec/strings.xml | 10 ++--- app/src/main/res/values-xal/strings.xml | 8 ++-- app/src/main/res/values-xmf/strings.xml | 6 +-- app/src/main/res/values-zgh/strings.xml | 2 +- app/src/main/res/values-zh/strings.xml | 20 ++++++++-- 90 files changed, 445 insertions(+), 480 deletions(-) diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 22f382f576..9ff1b19b4c 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭаларҭа Иҟаҵатәуп арегистрациа Асистемахь аҭаларҭа - Шәааԥшы ԥыҭрак… + Шәааԥшы ԥыҭрак... Аҭалара қәҿиарала имҩаԥысит! Асистемахь аҭалараан агха! Афаил ԥшаам. Даҽа фаилк шәахәаԥш. @@ -64,7 +64,7 @@ Ари шәара еилышәкаама? Ааи! Акатегориақәа - Аҭагалара… + Аҭагалара... Акагь алхӡам Иҟам ахҳәаа Идырым алицензиа diff --git a/app/src/main/res/values-af/strings.xml b/app/src/main/res/values-af/strings.xml index 57ba77cc93..1da8b3101f 100644 --- a/app/src/main/res/values-af/strings.xml +++ b/app/src/main/res/values-af/strings.xml @@ -22,6 +22,7 @@ %1$d lêers aan die uploaden + \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -147,7 +148,7 @@ Ja! <u>Meer inligting</u> Kategorieë - Laai … + Laai ... Niks gekies nie Geen beskrywing Geen bespreking nie diff --git a/app/src/main/res/values-anp/strings.xml b/app/src/main/res/values-anp/strings.xml index e4029af9b0..70a01949f2 100644 --- a/app/src/main/res/values-anp/strings.xml +++ b/app/src/main/res/values-anp/strings.xml @@ -27,14 +27,14 @@ पासवर्ड भूलाय गेलौ की? साइन अप करौ प्रवेश होय रहलौ छौं - कृपया प्रतीक्षा करौ… - कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ... + कृपया प्रतीक्षा करौ... प्रवेश विफल अपलोड आरंभ! हाल केरौ अपलोड कतारबद्ध विफल - अपलोड होय रहलौ छौं… + अपलोड होय रहलौ छौं... ठामे मँ हमरौ अपलोड साझा करौ @@ -68,7 +68,7 @@ हाँव! बेसी जानकारी श्रेणी सिनी - लोड होय रहलौ छौं… + लोड होय रहलौ छौं... कुछु चयनित नाय कोय शीर्षक नाय कोय विवरण नाय @@ -173,7 +173,7 @@ पूर्ण होलौं अगलका छवि हाँव, केन्हअ नाय - कृपया प्रतीक्षा करौ… + कृपया प्रतीक्षा करौ... प्रतिलिपि बनैलौ गेलै! लेखक स्थान diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index b0fda69907..46ffcb7f56 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار… + يرجى الانتظار... نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -530,7 +530,7 @@ عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار… + الرجاء الانتظار... الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +552,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر… + مشاركة التطبيق عبر... معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -695,7 +695,7 @@ وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل … + جاري استئناف التحميل ... جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 63aa12b964..960b55bdad 100644 --- a/app/src/main/res/values-as/strings.xml +++ b/app/src/main/res/values-as/strings.xml @@ -27,7 +27,7 @@ পাছৱৰ্ড পাহৰিলে? পঞ্জীয়ন কৰক লগইন হৈ আছে - অনুগ্ৰহ কৰি অপেক্ষা কৰক… + অনুগ্ৰহ কৰি অপেক্ষা কৰক... লগইন সফল হ\'ল! লগইন বিফল হৈছে! ফাইল পোৱা নগ\'ল। অনুগ্ৰহ কৰি আন এটা ফাইল চেষ্টা কৰক। @@ -74,7 +74,7 @@ <u>গোপনিয়তা নীতি</u> প্ৰতিক্ৰিয়া প্ৰেৰণ কৰক (ইমেইল যোগে) কোনো ইমেইল ক্লায়েন্ট ইনষ্টল কৰা নাই - প্ৰথম চিংকৰ বাবে অপেক্ষাৰত… + প্ৰথম চিংকৰ বাবে অপেক্ষাৰত... আপুনি এতিয়ালৈকে কোনো ফটো আপল\'ড কৰা নাই। পুনৰ চেষ্টা কৰক বাতিল কৰক diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 34212aebbc..df61ed0610 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -74,7 +74,7 @@ Aniciando sesión Espera… Actualizando pies y descripciones - Porfavor espera… + Porfavor espera... ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. @@ -480,7 +480,7 @@ Númberos de serie Software Xubi semeyes a Wikimedia Commons direutamente dende\'l to móvil. Descarga yá la app de Commons: %1$s - Compartir l\'aplicación per… + Compartir l\'aplicación per... Información de la imaxe Nun s\'alcontró nenguna categoría Nun s\'alcontraron retratos diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index d2ea468ad7..1edbe43fce 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -104,7 +104,7 @@ CC BY 3.0 Əlavə məlumat Kateqoriyalar - Yüklənir… + Yüklənir... Heç biri seçilməmişdir Naməlum lisenziya Yenilə diff --git a/app/src/main/res/values-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 8e77643235..4fa660ef88 100644 --- a/app/src/main/res/values-b+roa+tara/strings.xml +++ b/app/src/main/res/values-b+roa+tara/strings.xml @@ -40,8 +40,8 @@ Tràse Passuord scurdate? Reggistrate - Stoche a tràse… - Aspitte… + Stoche a tràse... + Aspitte... E\' trasute! Non g\'è trasute! File non acchiate. Pruève \'n\'otre file. @@ -121,7 +121,7 @@ Permesse richieste Non ge tìne notifeche non lette Errore assute mendre ca ste pigghiave le immaggine - Aspitte… + Aspitte... Zumbe ste immaggine Autore Lènghe d\'a descrizione predefinite diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index b8b602d0dc..cd1cb09e8f 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -5,7 +5,7 @@ * Milicevic01 * Zoranzoki21 --> - + Fejsbuk stranica Ostave Izvorni kod na Github-u Logo Ostave @@ -26,39 +26,32 @@ Slika dana %1$d datoteka se otprema - %1$d datoteke se otpremaju %1$d datoteke se otpremaju %1$d otpremanje - %1$d otpremanja %1$d otpremanja Pokretanje otpremanja Procesuiranje %d otpremanje - Procesuiranje %d otpremanja Procesuiranje %d otpremanja %d otpremanje - %d otpremanja %d otpremanja Slika će se voditi pod licencom %1$s - Slike će se voditi pod licencom %1$s Slike će se voditi pod licencom %1$s %1$d otpremanje - %1$d otpremanja %1$d otpremanja - Primanje deljenog sadržaja… Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja - Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja - Primanje deljenog sadržaja… Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja + Primanje deljenog sadržaja... Procesuiranje slike može potrajati neko vreme, u zavisnosti od veličine slike i vašeg uređaja + Primanje deljenog sadržaja... Procesuiranje slika može potrajati neko vreme, u zavisnosti od veličine slika i vašeg uređaja Istraga Izgled @@ -493,7 +486,7 @@ Pristup lokaciji medija je odbijen Možda nećemo moći da automatski pribavimo podatke o lokaciji iz slika koje otpremite. Dodajte odgovarajuću lokaciju za svaku sliku pre objavljivanja Otpremi fotografije na Vikimedijinu Ostavu direktno sa svog telefona. Preuzmi aplikaciju Ostave sada: %1$s - Podeli aplikaciju preko… + Podeli aplikaciju preko... Informacije o slici Nisu pronađene kategorije Otkazano otpremanje @@ -518,13 +511,12 @@ Uspešno Kategorija %1$s je dodata. - Kategorije %1$s su dodate. Kategorije %1$s su dodate. Nije moguće dodati kategorije. Ažuriraj kategoriju Uredi prikaze - Pokušavanje promena koordinata… + Pokušavanje promena koordinata... Ažuriranje koordinata Ažuriranje opisa Ažuriranje natpisa @@ -706,7 +698,6 @@ Nije moguće podeliti ovu stavku %d slika je odabrana - %d slika je odabrano %d slika je odabrano diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 4c33b396fe..0fc68329f1 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -61,9 +61,9 @@ Серһүҙҙе оноттоғоҙмо? Теркәлеү Системаға инеү - Зинһар, көтөгөҙ… + Зинһар, көтөгөҙ... Аңлатмалар һәм тасуирламалар яңыртыла - Зинһар, көтөгөҙ… + Зинһар, көтөгөҙ... Системаға инеү уңышлы! Системаға инеү уңышһыҙ! Файл табылманы. Башҡа файлды эҙләп ҡарағыҙ. @@ -131,7 +131,7 @@ Фекереңде ебәр (эл.почта аша) Почта клиенты асыҡланмаған Яңыраҡ ҡулланылған категориялар - Тәүге синхронлаштырыуҙы көтөү… + Тәүге синхронлаштырыуҙы көтөү... Әлегә бер фото ла йөкләмәгәнһегеҙ Ҡабатларға Кире алыу @@ -171,7 +171,7 @@ Эйе! Ентеклерәк Категориялар - Йөкләнә башланы… + Йөкләнә башланы... Бер ни ҙә һайланмаған Тасуирламаһы юҡ Фекер алышыу юҡ diff --git a/app/src/main/res/values-ban/strings.xml b/app/src/main/res/values-ban/strings.xml index 1273eaf0d6..b24bc00221 100644 --- a/app/src/main/res/values-ban/strings.xml +++ b/app/src/main/res/values-ban/strings.xml @@ -61,7 +61,7 @@ Lali kruna Sandi? Daftar Ngeranjingin log - Jantos dumun… + Jantos dumun... Nganyarin sesirah miwah pidarta Jantos dumun… Mahasil manjing log! @@ -303,7 +303,7 @@ Nomor seri Piranti lunak Unggah foto nuju Wikimédia Commons langsung saking télépon ragané. Unduh aplikasi Commons mangkin: %1$s - Wedar aplikasi saking… + Wedar aplikasi saking... Pidarta Gambar Pangunggahan Kawangdé %1$s kaunggah olih: %2$s @@ -340,7 +340,7 @@ Kaanggén Paringkat Titiang Kualitas Gambar - Ngalanturang unggahan… + Ngalanturang unggahan... Ngarérénang unggahan… Wangdé Unggah Lisénsi Média diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cb19d6e39f..6ee9315423 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -304,7 +304,7 @@ Преглеждане на прочетени Преглеждане на непрочетени Възникна грешка при избирането на изображенията - Моля, изчакайте… + Моля, изчакайте... напълно размазано Наблизо Прочетете повече diff --git a/app/src/main/res/values-blk/strings.xml b/app/src/main/res/values-blk/strings.xml index 51a8ec1ba8..2cf4aba5bd 100644 --- a/app/src/main/res/values-blk/strings.xml +++ b/app/src/main/res/values-blk/strings.xml @@ -38,7 +38,7 @@ အွောန်ႏဖေင်ꩻထိုꩻ ငဝ်းဗိဉ်ႏပလို့ꩻနဲ့? ဒင်ႏမတ်ပိုင်တိဉ် အဝ်ႏနွို့အကောက်ကျာꩻ - အိုင်ပွေားဆောင်းတဆင်ႏသြ… + အိုင်ပွေားဆောင်းတဆင်ႏသြ... နွို့အကောက်အောင်ႏလဲဉ်း! နွို့အကောက်အောင်ႏတဝ်း! မော့ꩻတဝ်းဖုဲင်၊ စံꩻထွားစံꩻသွော့ ဖုဲင်အလင်တဗာႏသြ။ @@ -97,7 +97,7 @@ မွေး! ထဲင်းယင်း သꩻတင်ꩻအချက်လက် ကဏ္ဍဖုံႏ - အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ… + အဝ်ႏဒင်ႏဝွန်ႏကျာꩻ... လွိုက်ခါꩻတဝ်းမုဲင်ꩻမုဲင်ꩻ ပုင်ႏလိတ်အဝ်ႏတဝ်း အွောန်ႏနယ်ချက်အဝ်ႏတဝ်း diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 51502c2649..2d156c1999 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -393,7 +393,7 @@ কোনও চিত্র ব্যবহৃত হয়নি পঠিতগুলি দেখান অপঠিতগুলি দেখান - অনুগ্রহ করে অপেক্ষা করুন… + অনুগ্রহ করে অপেক্ষা করুন... অনুলিপি করা হয়েছে এই চিত্র এড়িয়ে যান প্রণেতা diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 9537c45e62..1c7d09617a 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -40,9 +40,6 @@ %1$d bellgargadenn loc\'het - %1$d bellgargadenn loc\'het - %1$d bellgargadennoù loc\'het - %1$d bellgargadennoù loc\'het %1$d pellgargadennoù loc\'het @@ -54,9 +51,6 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ - gant an aotre-implijout %1$s e vo an div skeudenn-mañ - gant an aotre-implijout %1$s e vo meur a skeudenn-mañ - gant an aotre-implijout %1$s e vo kalz a skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ Ergerzhout diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index d178ff507a..91860b1e12 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -10,22 +10,19 @@ Logo Commonsa postavlja se %1$d datoteka - postavlja se %1$d datoteke postavlja se %1$d datoteka + \@string/contributions_subtitle_zero postavljena %1$d datoteka - postavljena %1$d datoteke postavljenih datoteka: %1$d Započinjem postavljanje %1$d datoteke - Započinjem postavljanje %1$d datoteke Započinjem postavljanje %1$d datoteka/-e %1$d postavljanje - %1$d postavljanja %1$d postavljanja Slika će se voditi pod licencom %1$s diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 51330cb9d2..0c15e58c34 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,33 +20,27 @@ Imatge del dia s\'està carregant %1$d fitxer - S\'estan carregant de %1$d fitxers s\'estan carregant %1$d fitxers (%1$d) - (%1$d) (%1$d) S\'inicien les càrregues S\'està processant %1$d càrrega - S\'estan processant %1$d càrregues S\'estan processant %1$d càrregues %d càrrega - $d càrregues %d càrregues Aquesta imatge quedarà sota llicència %1$s - Aquestes imatges quedaran sota llicència %1$s Aquestes imatges quedaran sota llicència %1$s %1$d pujada - %1$d pujades %1$d pujades Explora @@ -398,7 +392,7 @@ Model de lent Números de sèrie Programari - Comparteix l\'aplicació a través de… + Comparteix l\'aplicació a través de... Informació de la imatge No s’ha trobat cap categoria No s\'han trobat representacions diff --git a/app/src/main/res/values-ce/strings.xml b/app/src/main/res/values-ce/strings.xml index e25b83e259..e13e8c040d 100644 --- a/app/src/main/res/values-ce/strings.xml +++ b/app/src/main/res/values-ce/strings.xml @@ -64,7 +64,7 @@ Викиларма Параметраш Викиларма чуйаккха - ДӀадоьдуш ду чуйаккхар… + ДӀадоьдуш ду чуйаккхар... Декъашхочун цӀе Пароль Commons Beta тӀехь хьай цӀарца чугӀо @@ -146,7 +146,7 @@ ЦӀе: Сиднейн операн театр ХӀаъ! Категореш - Чуйолуш… + Чуйолуш... ХӀума хаьржина йац Куьг доцуш Хаамаш бац @@ -297,7 +297,7 @@ Серийн лоьмар Программан кхачам Файл йолу меттиган тӀекхача бакъо ца ло - Йекъа программа, гӀоьнца… + Йекъа программа, гӀоьнца... Суьртан информаци Цхьа а категори ца карийна. Цхьа а хаам ца карийна. @@ -362,7 +362,7 @@ ДӀайаьккхина закладки йукъара Цхьа хӀума галдаьлла. Фонан сурт хӀотто аьтто ца баьлла Фонан сурт санна хӀоттайе - Фонан сурт дӀахӀоттош ду… + Фонан сурт дӀахӀоттош ду... Системин нисдаран гӀирс Бодане Сирла diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index fb4ee05ef3..4d49ee6327 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -32,44 +32,31 @@ Obrázek dne %1$d soubor se nahrává - %1$d soubory se nahrávají - %1$d souborů se nahrává %1$d souborů se nahrává + \@string/contributions_subtitle_zero (%1$d) - (%1$d) - (%1$d) (%1$d) Spouští se nahrávání %1$d souboru - Spouští se nahrávání %1$d souborů - Spouští se nahrávání %1$d souborů Spouští se nahrávání %1$d souborů %1$d nahrávání - %1$d nahrávání - %1$d nahrávání %1$d nahrávání Tento obrázek bude zveřejněn pod licencí %1$s - Tyto obrázky budou zveřejněny pod licencí %1$s - Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s %1$d nahrání - %1$d nahrávání - %1$d nahrávání %1$d nahrání Probíhá příjem sdíleného obsahu. Zpracování obrázku může chvíli trvat v závislosti na velikosti obrázku a vašem zařízení - Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení - Probíhá příjem sdíleného obsahu. Zpracovávání obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Probíhá příjem sdíleného obsahu. Zpracování obrázků může chvíli trvat v závislosti na velikosti obrázků a vašem zařízení Objevit diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 7cdfb13825..623a48c8c2 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -29,7 +29,7 @@ Wlogùjë mie Wregistrëjë sã Logòwanié - Proszã żdac… + Proszã żdac... Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. @@ -78,7 +78,7 @@ Sélôj òpinijã (przez e-mail) Felënk wjinstalowónegò e-mailowégò klienta Slédno ùżëwóne kategòrëje - Żdanié na pierszą synchronizacëjã… + Żdanié na pierszą synchronizacëjã... Nie môsz jesz wladowónych òdjimków Próbùjë znowa Òprzestóń @@ -99,7 +99,7 @@ Przëmiôr wladënka: Jo! Kategòrëje - Wladënk… + Wladënk... Felënk nacéchòwaniô Felënk òpisënka Nieznónô licencëja diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 50df9b6d8c..8c4b4a652a 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -21,44 +21,25 @@ Popeth Llun y Dydd - %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho - %1$d ffeil yn uwchlwytho %1$d ffeil yn uwchlwytho \@string/contributions_subtitle_zero (%1$d) - (%1$d) - (%1$d) - (%1$d) (%1$d) Cychwyn Uwchlwytho - Dechrau %1$d uwchlwythiad Cychwyn %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad - Dechrau %1$d uwchlwythiad Cychwyn uwchlwytho %1$d ffeil - %1$d uwchlwythiad %1$d uwchlwythiad - %1$d uwchlwythiad - %1$d uwchlwythiad - %1$d uwchlwythiad %1$d uwchlwythiad - Ni chaiff unrhyw ddelweddau eu trwyddedu dan %1$s Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s - Caiff y delweddau hyn eu trwyddedu dan %1$s Caiff y delweddau hyn eu trwyddedu dan %1$s Archwilio diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 80a82afb54..3b6822c47b 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -507,7 +507,7 @@ Adgang til medieplacering nægtet Vi kan muligvis ikke automatisk indhente placeringsdata fra billeder, du uploader. Tilføj den passende placering for hvert billede, før du indsender Upload billeder til Wikimedia Commons direkte fra din telefon. Download Commons-appen nu: %1$s - Del app via… + Del app via... Billedoplysninger Ingen kategorier blev fundet Ingen afbildninger fundet @@ -642,9 +642,9 @@ Begrænset forbindelsestilstand Kvalitetsbilleder Kvalitetsbilleder er tegninger eller fotografier, der opfylder visse kvalitetsstandarder (som for det meste er af teknisk karakter) og er værdifulde for Wikimedia-projekter - Genoptager upload… - Sætter upload på pause… - Annullerer upload… + Genoptager upload... + Sætter upload på pause... + Annullerer upload... Annuller upload Du har aktiveret begrænset forbindelsestilstand. Alle uploads er sat på pause og genoptages, når du deaktiverer denne tilstand. Begrænset forbindelsestilstand aktiveret! @@ -784,7 +784,7 @@ Andet problem eller anden information (forklar venligst nedenfor). Din feedback bliver slået op på følgende wiki-side: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Er du sikker på, at du vil annullere alle uploads? - Annullerer alle uploads… + Annullerer alle uploads... Uploads Afventer Mislykkedes diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 4043040214..a6471c1fe9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -516,7 +516,7 @@ Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten Bitte warten … - Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. + Vorgestellte Bilder sind Bilder von professionellen Fotografen und Zeichnern, die die Gemeinschaft von Wikimedia Commons als diejenigen mit der höchsten Qualität auf der Website ausgewählt hat. Über Orte in der Nähe hochgeladene Bilder sind die Bilder, die von entdeckten Orten auf der Karte hochgeladen wurden. Diese Funktion erlaubt es Autoren, eine Dankeschön-Benachrichtigung an Benutzer zu senden, die nützliche Bearbeitungen durchgeführt haben – durch die Benutzung eines kleinen Dankeschön-Links in der Versionsgeschichte oder Unterschiedsseite. Auf Folgemedien kopieren @@ -611,7 +611,7 @@ zu den Lesezeichen hinzugefügt Etwas ist schiefgelaufen. Das Hintergrundbild konnte nicht eingestellt werden Als Hintergrundbild festlegen - Hintergrundbild wird festgelegt. Bitte warten… + Hintergrundbild wird festgelegt. Bitte warten... Systemeinstellung Dunkel Hell diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index ebb3cfbe4e..840b0198dc 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -62,8 +62,8 @@ Parola, xo vira kerde? Qeyd be Kewno cı - Kerem kerên, bıpawên… - Kerem ke, bıpawe… + Kerem kerên, bıpawên... + Kerem ke, bıpawe... Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. @@ -93,7 +93,7 @@ Şınasnayış Bınnuşte Xırabiya kewten-network xeta - Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2–3 deqey ra tepeya reyna bıcerrebnên. + Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Kodê kamiya raştkerdışi dıfaktorın gani cı kewê. Nidekeweya de @@ -298,7 +298,7 @@ Pêhesnayışê toyê wendışi çıniyê Wendışi bıvêne Nêwendeyan bıvêne - Kerem kerên, bıpawên… + Kerem kerên, bıpawên... Nê resımi raviyarnê Nuştekar Heqa telifi diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index e4b597fb12..e3675ef0a3 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -91,7 +91,7 @@ Σύνδεση Ξεχάσατε τον κωδικό πρόσβασης σας; Εγγραφή - Γίνεται σύνδεση… + Γίνεται σύνδεση... Παρακαλούμε αναμείνετε… Ενημέρωση λεζάντων και περιγραφών Παρακαλούμε αναμείνετε… @@ -204,7 +204,7 @@ Ναι! Περισσότερες πληροφορίες Κατηγορίες - Φόρτωση σε εξέλιξη… + Φόρτωση σε εξέλιξη... Καμία επιλεγμένη Χωρίς λεζάντα Χωρίς περιγραφή @@ -521,7 +521,7 @@ Δεν επιτρέπεται η πρόσβαση στην τοποθεσία πολυμέσων Ενδέχεται να μην μπορούμε να λάβουμε αυτόματα δεδομένα τοποθεσίας από φωτογραφίες που ανεβάζετε. Προσθέστε την κατάλληλη τοποθεσία για κάθε εικόνα πριν την υποβολή Ανεβάστε φωτογραφίες στα Wikimedia Commons απευθείας από το τηλέφωνό σας. Κάντε λήψη της εφαρμογής Commons τώρα: %1$s - Κοινή χρήση εφαρμογής μέσω… + Κοινή χρήση εφαρμογής μέσω... Πληροφορίες Εικόνας Δεν βρέθηκαν Κατηγορίες Δεν βρέθηκαν απεικονίσεις @@ -798,7 +798,7 @@ Άλλο πρόβλημα ή πληροφορίες (παρακαλούμε εξηγήστε παρακάτω). Τα σχόλιά σας δημοσιεύονται στην ακόλουθη σελίδα wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Εφαρμογή για κινητά/Σχόλια</a> Είστε βέβαιοι ότι θέλετε να ακυρώσετε όλες τις μεταφορτώσεις; - Ακύρωση όλων των μεταφορτώσεων… + Ακύρωση όλων των μεταφορτώσεων... Μεταφορτώσεις Σε εκκρεμότητα Απέτυχε diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 323c823b25..69673afbed 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -78,7 +78,7 @@ Ĉu pasvorto forgesita? Registriĝi Ensalutado - Bonvolu atendi… + Bonvolu atendi... Ĝisdatiganta subtekstojn kaj priskribojn Bonvolu atendi… Ensalutado sukcesis @@ -150,7 +150,7 @@ Sendi viajn komentojn (per retpoŝto) Neniu retpoŝtilo instalita Laste uzitaj kategorioj - Atendas la unuan Sinkronigado… + Atendas la unuan Sinkronigado... Vi ankoraŭ ne alŝutis fotojn. Reprovi Nuligi @@ -190,7 +190,7 @@ Jes! <u>Ekscii pli</u> Kategorioj - Ŝargado… + Ŝargado... Neniu elektita Neniu substeksto Sen priskribo @@ -482,7 +482,7 @@ Vidu legitajn Vidi nelegitojn Eraro okazis dum elektado de bildoj - Bonvolu atendi… + Bonvolu atendi... Elstaraj bildoj estas tiuj bildoj far tre spertaj fotografistoj kaj ilustristoj, kiujn la komunumo de Vikimedia Komunejo elektis kiel iujn de la plej alta kvalito en la retejo. Bildoj Alŝutitaj per Apudaj lokoj estas bildoj alŝutitaj per trovado de lokoj sur la mapo. Tiu funkcio ebligas sendi Dankantan sciigon al farinto de utila redakto – per malgranda dankiga ligilo ĉe la paĝo de historio aŭ diferenco. @@ -504,7 +504,7 @@ Aliro al loko de plurmediaĵo malakceptita Ni eble ne povos aŭtomate akiri pri-lokajn datumojn de bildoj, kiujn vi alŝutas. Bonvolu aldoni la taŭgan lokon por ĉiu bildo antaŭ ol sendi Alŝutu fotojn al Vikimedia Komunejo rekte de via telefono. Elŝutu la Komunejan aplikaĵon nun: %1$s - Diskonigi aplikaĵon per… + Diskonigi aplikaĵon per... Informo pri Bildo Neniu Kategorio troviĝis Neniu bildo-priskribo trovita @@ -636,9 +636,9 @@ Modo por limigita konekto Kvalitaj Bildoj Kvalitaj bildoj estas diagramoj aŭ fotoj kiuj kontentigas certajn normojn pri kvalito (kiuj estas plejparte teknikaj) kaj estas valoraj por Vikimediaj projektoj. - Rekomencante alŝuton… - Paŭzante alŝuton… - Nuligante alŝuton… + Rekomencante alŝuton... + Paŭzante alŝuton... + Nuligante alŝuton... Ĉesigi alŝutadon Vi aktivigis Modon por limigita konekto. Ĉiuj alŝutoj estas paŭzitaj kaj rekomencos post kiam vi malŝaltos ĉi modon. Modo por limigita konekto estas aktivigita. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2189534705..4e90f68641 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -51,7 +51,7 @@ * Vivaelcelta * Wizardeck --> - + Página de Facebook de Commons Código fuente de Commons en GitHub Logo de Commons @@ -75,38 +75,31 @@ Foto del día Cargando %1$d archivo - Cargando %1$d archivos Cargando %1$d archivos (%1$d) - (%1$d) (%1$d) Comenzando las subidas Procesando %d carga - Procesando %d cargas Procesando %d cargas %d carga - %1 cargas %1 cargas Esta imagen se publicará bajo la licencia %1$s - Estas imágenes se publicarán bajo la licencia %1$s Estas imágenes se publicarán bajo la licencia %1$s %1$d Subida - %1$d Subidas %1$d Subidas Recepción de contenido compartido. El procesamiento de la imagen puede tardar cierto tiempo, dependiendo del tamaño de la imagen y de tu dispositivo - Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Recepción de contenido compartido. El procesamiento de las imágenes puede tardar cierto tiempo, dependiendo del tamaño de las imágenes y de tu dispositivo Explorar @@ -342,7 +335,7 @@ Omitir tutorial Internet no disponible Error al recuperar las notificaciones - Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. + Hubo un error al recuperar la imágen a revisar. Toca refrescar para intentarlo de nuevo. No se encontró ninguna notificación Traducir Idiomas @@ -484,7 +477,7 @@ Permitir Descartar Por favor, activa el acceso a la ubicación desde Configuración y vuelva a intentarlo. \n\nNota: Es posible que la subida no tenga datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. - La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. + La cámara dentro de la aplicación necesita el permiso a la ubicación para adjuntarla a sus imágenes en caso de que la ubicación no esté disponible en EXIF. Por favor, permita que la aplicación acceda a su ubicación e inténtelo de nuevo.\n\nNota: Es posible que la subida no tenga los datos de la la ubicación si la aplicación no puede recuperar la ubicación del dispositivo en un intervalo corto. La aplicación no registrará la ubicación junto con las tomas debido a la falta del permiso de la ubicación. La aplicación no registrará la ubicación junto con las tomas porque el GPS está apagado Utilizar el selector de fotografías basado en documentos @@ -512,8 +505,8 @@ ¿Está correctamente categorizado? ¿Está dentro de los objetivos del proyecto? ¿Quieres agradecer al colaborador? - Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. - Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado + Toca en NO para nominar esta imágen para ser borrada si no es para nada útil. + Los logotipos, las capturas de pantalla y los pósteres de películas son habitualmente infracciones a los derechos de autor.\n Toca NO para nominar esta imágen para borrado Tu apreciación animara a %1$s ¡Oh, esto ni siquiera esta categorizado! Esta imagen esta dentro de %1$s categorías. @@ -531,7 +524,7 @@ Compartir registros usando Ver leídas Ver no leidas - Ocurrió un error mientras se elegían imágenes + Ocurrió un error mientras se elegían imagenes Un momento… Las imágenes destacadas son creaciones de talentosos fotógrafos e ilustradores que la comunidad de Wikimedia Commons ha reconocido como las de mayor calidad del sitio. Las imágenes subidas vía Lugares Cercanos son las imágenes que han sido subidas al descubrir lugares en el mapa. @@ -554,7 +547,7 @@ Acceso a la ubicación del archivo multimedia denegado Es posible que no podamos obtener automáticamente los datos de ubicación de las imágenes que suba. Añada la ubicación adecuada a cada imagen antes de enviarla Sube fotos a Wikimedia Commons directamente desde tu celular. Descarga la aplicación de Commons ahora: %1$s - Compartir la aplicación vía… + Compartir la aplicación vía... Información de la imagen No se encontró ninguna categoría No se encontraron representaciones @@ -581,7 +574,6 @@ Éxito Se añade %1$s categoría. - Se añaden %1$s categorías. Se añaden %1$s categorías. No se pudieron añadir las categorías. @@ -590,7 +582,6 @@ Editar las descripciones %1$s Se añade la descripción. - Descripción %1$s se añadieron. Descripción %1$s se añadieron. No se pueden añadir descripciones. @@ -608,7 +599,7 @@ Las coordenadas de la imagen no están actualizadas. No se puede obtener descripciones. Editar descripciones y leyendas - Compartir imagen via + Compartir imagen via Todavía no has hecho ninguna contribución. %s Aún no ha realizado ninguna contribución Cuenta creada @@ -633,7 +624,7 @@ añadido a marcadores Algo salió mal. No se pudo establecer el fondo de pantalla Colocar como fondo de pantalla - Estableciendo el fondo de pantalla. Por favor espere… + Estableciendo el fondo de pantalla. Por favor espere... Seguir sistema Oscuro Claro @@ -691,9 +682,9 @@ Modo de conexión limitada Imágenes de calidad Las imágenes de calidad son diagramas o fotografías que cumplen determinados estándares de calidad (mayormente de carácter técnico) y que son valiosas para proyectos de Wikimedia - Reanudando carga… - Pausando carga… - Cancelando carga… + Reanudando carga... + Pausando carga... + Cancelando carga... Cancelar carga Has habilitado el modo de conexión limitada. Todas las cargas están pausadas y se reanudarán cuando deshabilites este modo. El modo de conexión limitada está encendido. @@ -820,8 +811,7 @@ Guardar archivo GPX %d imagen seleccionada - %d imágenes seleccionadas - %d imágenes seleccionadas + %d imagenes seleccionadas Recuerde que todas las imágenes en una carga múltiple tienen la misma categoría y representación. Si las imágenes no comparten representación y categoría, haga varias cargas por separado. Nota sobre cargas múltiples @@ -829,7 +819,7 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. - Cancelando todas las subidas… + Cancelando todas las subidas... Subidas Pendiente Falló diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 3dd463b345..ff75cbc7f6 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -154,7 +154,7 @@ Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: Naturako elementuak (loreak, animaliak, mendiak) Objektu erabilgarriak (bizikletak, tren geltokiak) - Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat…) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) Mesedez EZ igo: Autorretratuak edo zure lagunen argazkiak Internetetik jaitsitako irudiak diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 4cdd2b87a9..841160581f 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -83,7 +83,7 @@ رمز عبور خودتان را فراموش کرده‌اید؟ ثبت نام واردشدن - شکیبا باشید… + شکیبا باشید... ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. @@ -122,7 +122,7 @@ تغییرها بارگذاری جستجوی رده‌ها - جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، …) + جستجو برای موضوعی که در پروندهٔ شما به‌نمایش کشیده‌شده است (مثلا کوه، تاج محل، ...) ذخیره تازه کردن فهرست @@ -411,7 +411,7 @@ شما هیچ اعلان خوانده‌شده‌ای ندارید نمایش دیده‌شده مشاهده خوانده نشده ها - لطفاً صبر کنید… + لطفاً صبر کنید... نمونه تصاویری که برای بازگذاری مناسب نیستند از این تصویر صرف نظر کن مدیریت تگ‌های EXIF @@ -423,7 +423,7 @@ مدل لنز شماره سریال نرم‌افزار - اشتراک از طریق… + اشتراک از طریق... اطلاعات عکس هیچ رده‌ای یافت نشد بارگذاری لغو شد @@ -455,7 +455,7 @@ به بوکمارک‌ها افزوده شد مشکل به وجود آمد. به عنوان پس‌زمینه انتخاب نشد. انتخاب به عنوان پس‌زمینه - قرار دادن پس‌زمینه. لطفاً صبر کنید… + قرار دادن پس‌زمینه. لطفاً صبر کنید... سامانه را دنبال کنید تیره روشن diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 26328a3e2c..312ebc84c5 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -80,7 +80,7 @@ Kirjaudutaan Odota… Päivitetään kuvatekstejä ja kuvauksia - Odota… + Odota... Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. @@ -481,7 +481,7 @@ Sarjanumerot Ohjelmisto Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s - Jaa sovellus… + Jaa sovellus... Kuvan tiedot Luokkia ei löytynyt Kuvauksia ei löytynyt @@ -546,7 +546,7 @@ Lisätty kirjanmerkkeihin Jotain meni väärin. Ei voitu asettaa taustakuvaksi. Aseta taustakuvaksi - Asetetaan taustakuvaksi. Odota… + Asetetaan taustakuvaksi. Odota... Käytä järjestelmän Tumma Vaalea @@ -594,8 +594,8 @@ Rajoitettu yhteistila pois päältä. Jonossa olevat lähetykset kopioidaan nyt. Rajoitettu yhteystila Laatukuvat - Jatketaan lähettämistä… - Keskeytetään lähetys… + Jatketaan lähettämistä... + Keskeytetään lähetys... Peruutetaan tallennusta… Peruuta tallennus Rajoitettu yhteystila on päällä. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 995e4041b3..ae4dfb9664 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -46,7 +46,7 @@ * Wladek92 * Y-M D --> - + Page Facebook de Commons Code source Github de Commons Logo de Commons @@ -70,38 +70,31 @@ Image du jour %1$d fichier en cours de téléversement - %1$d fichiers en cours de téléversement %1$d fichiers en cours de téléversement (%1$d) - (%1$d) (%1$d) Démarrage des téléversements %d téléversement en cours - %d téléversements en cours %d téléversements en cours %d téléversement - %d téléversements %d téléversements Cette image sera sous licence %1$s. - Ces images seront sous licence %1$s. Ces images seront sous licence %1$s. %1$d téléversement - %1$d téléversements %1$d téléversements - Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. - Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. + Réception de contenu partagé. Le traitement de l’image peut prendre un certain temps en fonction de la taille de l’image et de votre matériel. Réception de contenu partagé. Le traitement des images peut prendre un certain temps en fonction de la taille des images et de votre matériel. Explorer @@ -120,9 +113,9 @@ Mot de passe oublié ? S’inscrire Connexion - Veuillez patienter… + Veuillez patienter... Mise à jour des légendes et des descriptions - Veuillez patienter… + Veuillez patienter... Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. @@ -192,7 +185,7 @@ Envoyer vos commentaires (par courriel) Aucun client de courriel installé Catégories récemment utilisées - En attente de première synchronisation… + En attente de première synchronisation... Vous n’avez encore téléchargé aucune photo. Réessayer Annuler @@ -232,7 +225,7 @@ Oui ! Davantage d’informations Catégories - Chargement en cours… + Chargement en cours... Aucune catégorie sélectionnée Aucune légende Aucune description @@ -528,7 +521,7 @@ Afficher les lus Afficher les non lus Une erreur est survenue lors de la sélection des images - Veuillez patienter… + Veuillez patienter... Les images en vedette sont des images de photographes et d’illustrateurs très doués que la communauté de Wikimédia Commons a choisies comme étant de la meilleure qualité pour le site. Les images téléversées par « Lieux à proximité » sont les images téléversées lors de la découverte de lieux sur la carte. Cette fonctionnalité permet aux contributeurs d’envoyer une notification de remerciement aux utilisateurs qui font des modifications utiles ― en utilisant un petit lien de remerciement sur la page historique ou sur celle du diff. @@ -550,7 +543,7 @@ Accès à l’emplacement du média refusé Nous ne pourrons pas obtenir automatiquement les données de localisation des images que vous téléchargez. Veuillez ajouter l’emplacement approprié pour chaque image avant de la soumettre. Téléversez des photos sur Wikimedia Commons directement depuis votre téléphone. Téléchargez l’application Commons maintenant : %1$s - Partager l’application via… + Partager l’application via... Informations sur l’image Aucune catégorie trouvée Aucun élément représenté trouvé @@ -577,7 +570,6 @@ Succès La catégorie %1$s est ajoutée. - Les catégories %1$s sont ajoutées. Les catégories %1$s sont ajoutées. Impossible d’ajouter des catégories. @@ -586,7 +578,6 @@ Modifier les éléments représentés L’élément représenté %1$s est ajouté. - Les éléments représentés %1$s sont ajoutés. Les éléments représentés %1$s sont ajoutés. Impossible d’ajouter des éléments représentés. @@ -629,7 +620,7 @@ Ajouté aux favoris Un problème est survenu. Impossible d’installer le fond d’écran. Définir comme fond d’écran - Installation du fond d’écran. Veuillez patienter… + Installation du fond d’écran. Veuillez patienter... Suivre le système Sombre Clair @@ -687,9 +678,9 @@ Mode de connexion limitée Images de qualité Les images de qualité sont des diagrammes ou des photographies qui respectent certains standards de qualité (qui sont, par nature, essentiellement techniques) et sont précieuses pour les projets Wikimedia. - Reprise du téléversement… - Mise en pause du téléversement… - Annulation du téléversement… + Reprise du téléversement... + Mise en pause du téléversement... + Annulation du téléversement... Annuler le téléversement Vous avez activé le mode de connexion limitée. Tous les téléversements sont suspendus et reprendront une fois ce mode désactivé. Le mode de connexion limitée est actif. @@ -818,7 +809,6 @@ Fichier GPX enregistré %d image sélectionnée - %d images sélectionnées %d images sélectionnées Souvenez-vous que toutes les images dans une importation multiple prennent les mêmes catégories et descriptions. Si les images de partagent pas les descriptions et catégories, veuillez effectuer plusieurs importations séparées. @@ -832,9 +822,12 @@ Autre problème ou information (merci d\'expliquer ci-dessous). Vos commentaires sont publiés sur la page wiki suivante : <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Êtes-vous sûr de vouloir annuler tous les téléchargements ? - Annulation de tous les téléchargements… + Annulation de tous les téléchargements... Téléversements En attente Échec Les données du lieu n\'ont pas pu être chargées + Cet endroit n\'a pas encore de photo, allez en prendre une ! + Cet endroit a déjà une photo. + Je vérifie maintenant si cet endroit a une photo. diff --git a/app/src/main/res/values-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index 4659eecf1a..b0ec664235 100644 --- a/app/src/main/res/values-gcr/strings.xml +++ b/app/src/main/res/values-gcr/strings.xml @@ -38,9 +38,9 @@ Ou bliyé ou Kodsigré ? Enskri oukò Konnègsyon - Souplé antann… + Souplé antann... Mizajou di léjann-yan ké dèskripsyon-yan - Souplé antann… + Souplé antann... Konnègsyon bon ! Konnègsyon pabon ! Fiché pa trouvé. Souplé éséyé ké rounòt. @@ -96,7 +96,7 @@ Enren ! Plis lenfòrmasyon Katégori-ya - Chajman ka fèt… + Chajman ka fèt... Pyès katégori sélègsyonnen Pyès léjann Pyès dèskripsyon diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index e11716a514..1740c1890c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -452,7 +452,7 @@ Modelo de lente Números de serie Software - Compartir a aplicación vía… + Compartir a aplicación vía... Información da imaxe Non se atoparon categorías Cancelouse a carga diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 2375838538..50a04319b9 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -39,6 +39,7 @@ %1$d फ़ाइलें अपलोड हो रहीं + \@string/contributions_subtitle_zero (%1$d) (%1$d) @@ -69,8 +70,8 @@ पासवर्ड भूल गये? खाता बनायें लॉग इन हो रहा है - कृपया प्रतीक्षा करें… - कृपया प्रतीक्षा करें… + कृपया प्रतीक्षा करें... + कृपया प्रतीक्षा करें... लॉग इन सफल! लॉग इन विफल! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। @@ -349,7 +350,7 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं… + सभी अपलोड रद्द किये जा रहे हैं... अपलोड लंबित विफल हुआ diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 414f0dd40a..d2d731c392 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -15,22 +15,19 @@ Slika dana Postavlja se %1$d datoteka - Postavlja se %1$d datoteke Postavljaju se %1$d datoteke + \@string/contributions_subtitle_zero %1$d postavljena datoteka - %1$d postavljena datoteke %1$d postavljene datoteke Započeto %1$d postavljanje - Započinjem %1$d postavljanja Započeta %1$d postavljanja %1$d postavljanje - %1$d postavljanja %1$d postavljanja Ova će slika biti licencirana pod %1$s @@ -49,7 +46,7 @@ Zaboravljena zaporka? Otvori račun Prijava - Molimo pričekajte … + Molimo pričekajte ... Prijava uspješna! Prijava neuspješna! Datoteka nije pronađena. Molimo probajte drugu. @@ -107,7 +104,7 @@ Pošaljite povratnu informaciju (putem elektroničke pošte) Klijent za elektroničku poštu nije instaliran Nedavno rabljene kategorije - Pričekajte za prvu sinkronizaciju… + Pričekajte za prvu sinkronizaciju... Nemate još postavljenih slika. Pokušaj ponovo Odustani @@ -147,7 +144,7 @@ Da! Više informacija Kategorije - Učitavanje… + Učitavanje... Ništa nije odabrano Nema opisa Nepoznata licencija @@ -196,7 +193,7 @@ Stranica datoteke na Zajedničkom poslužitelju Stavka na Wikidati Članak na Wikipediji - Opišite medij što je više moguće: gdje je napravljen, što prikazuje,… Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. Mogući problemi s ovom slikom: Slika je pretamna. Slika je mutna. @@ -284,7 +281,7 @@ Promijenio/la sam mišljenje, ne želim da više bude javno vidljivo Toliko ste pridonijeli projektu da se naš sustav za računanje postignuća ne može nositi s time. To je vrhunsko postignuće. Došlo je do pogrješke tijekom obradbe slike. Molimo Vas, pokušajte ponovo! - Molimo Vas, pričekajte … + Molimo Vas, pričekajte ... Preskoči ovu sliku Zadani jezik za opis Pokušavanje ažuriranja kategorija. @@ -296,7 +293,7 @@ Dodano u oznake Nešto je pošlo po zlu. Ne možemo postaviti pozadinu Postavi kao pozadinu - Postavljanje pozadine. Molimo, pričekajte… + Postavljanje pozadine. Molimo, pričekajte... Zadano Tamno Svijetlo diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index eb34386749..aefc17d9d5 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -441,7 +441,7 @@ Sorozatszámok Szoftver Képek feltöltése Wikimedia Commons-ba közvetlenül a telefonodról. Töltsd le a Commons applikációt most: %1$s - Alkalmazás megosztása ezzel… + Alkalmazás megosztása ezzel... Képinformáció Nem található kategória Megszakított feltöltés @@ -474,7 +474,7 @@ Híd, múzeum, szálloda, stb. A belépés nem sikerült, kérj új jelszót. Beállítás háttérképnek - Beállítás háttérképnek. Kérem várjon… + Beállítás háttérképnek. Kérem várjon... Rendszerbeállítás követése Sötét Világos diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 8fff554e31..219fa45210 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -61,6 +61,7 @@ %1$d Unggahan + Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Sedang menerima konten yang dibagikan. Memproses gambar mungkin memerlukan waktu lebih lama tergantung pada ukuran gambar dan perangkat Anda Jelajahi @@ -81,7 +82,7 @@ Memasuki log Silakan tunggu… Memperbarui takarir dan deskripsi - Mohon tunggu… + Mohon tunggu... Berhasil masuk log! Gagal masuk log! Berkas tidak ditemukan. Silakan coba berkas lain. @@ -190,7 +191,7 @@ Ya! Informasi selengkapnya Kategori - Memuat… + Memuat... Tidak ada yang dipilih Tanpa takarir Tidak ada keterangan @@ -496,7 +497,7 @@ Akses lokasi media ditolak Kami mungkin tidak dapat memperoleh data lokasi secara otomatis dari gambar yang Anda unggah. Harap tambahkan lokasi yang sesuai untuk setiap gambar sebelum mengirimkannya Mengunggah foto ke Wikimedia Commons secara langsung dari telepon Anda. Unduh aplikasi Commons sekarang: %1$s - Bagikan aplikasi lewat… + Bagikan aplikasi lewat... Info Gambar Kategori tidak ditemukan Penggambaran tidak ditemukan @@ -522,6 +523,7 @@ Pembaruan kategori Berhasil + Kategori %1$s ditambahkan. Kategori %1$s ditambahkan. Tidak bisa menambahkan kategori. @@ -567,7 +569,7 @@ Ditambahkan ke pembatas Terjadi kesalahan. Tidak bisa menetapkan wallpaper Jadikan Wallpaper - Sedang menetapkan Wallpaper. Tolong tunggu… + Sedang menetapkan Wallpaper. Tolong tunggu... Ikuti sistem Gelap Terang @@ -623,9 +625,9 @@ Mode Koneksi Terbatas Gambar Berkualitas Gambar berkualitas adalah diagram atau foto yang memenuhi standar kualitas tertentu (yang sifatnya teknis) dan berharga bagi proyek Wikimedia - Melanjutkan unggahan… - Menunda unggahan… - Membatalkan pengunggahan… + Melanjutkan unggahan... + Menunda unggahan... + Membatalkan pengunggahan... Batalkan pengunggahan Anda menyalakan mode koneksi terbatas. Semua pengunggahan ditunda dan akan dilanjutkan begitu Anda mematikan mode ini. Mode sambungan terbatas sedang menyala. @@ -741,7 +743,7 @@ %d gambar dipilih Bicara - Membatalkan semua unggahan… + Membatalkan semua unggahan... Unggahan Menunggu Gagal diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 994b1c3d34..51fe164419 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -70,9 +70,9 @@ Ka tu obliviis tua pasovorto? Enirar Eniranta - Voluntez vartar… + Voluntez vartar... Aktualiganta etiketi e deskripturi - Voluntez vartar… + Voluntez vartar... Eniro sucesoza! Eniro faliis! Arkivo ne trovita. Voluntez probar altr arkivo. @@ -142,7 +142,7 @@ Sendez komenti (per e-posto) Nula kliento di e-posto instalesis Kategorii recente uzita - Vartanta unesma sinkronigo… + Vartanta unesma sinkronigo... Vu ankore ne sendis fotografuri. Riprobar Nuligar @@ -180,7 +180,7 @@ Yes! Plusa informo Kategorii - Karganta… + Karganta... Nulo selektesis Nula deskripto-texto Nula deskripto @@ -410,7 +410,7 @@ Vu ne lektis irga avizo Vidar lektita Vidar ne-lektata - Vartez… + Vartez... Kopiita Exempli pri bona imaji por sendar a Commons Saltez ca imajo @@ -472,7 +472,7 @@ Ajusti Adjuntita marko-rubandi Uzar kom skreno-kovrilo - Kreanta skreno-kovrilo. Voluntez vartar… + Kreanta skreno-kovrilo. Voluntez vartar... Koloro obskura Koloro klara Charjez pluse @@ -500,7 +500,7 @@ Uzita Mea rango Imaji di qualeso - Nuliganta sendajo… + Nuliganta sendajo... Cesar kargajo Lektez pluse En omna idiomi diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ac64fbf2cd..4176529534 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ * Sveinki * Sveinn í Felli --> - + Commons Facebook-síðan Grunnkóði Commons á Github Táknmerki Commons @@ -51,7 +51,7 @@ %1$d innsendingar - Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns + Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndarinnar og gerð tækisins þíns Tek við deildu efni. Meðhöndlun myndarinnar gæti tekið einhvern tíma, sem fer eftir stærð myndaanna og gerð tækisins þíns Uppgötva @@ -138,7 +138,7 @@ Senda umsögn (með tölvupósti) Ekkert tölvupóstforrit er uppsett Nýlega notaðir flokkar - Bíð eftir fyrstu samstillingu… + Bíð eftir fyrstu samstillingu... Þú ert ekki ennþá búin(n) að senda inn neinar myndir. Reyna aftur Hætta við @@ -477,7 +477,7 @@ Hugbúnaður Aðgangi að staðsetningu gagnamiðla hafnað Sendu myndir inn á Wikimedia Commons beint úr símanum þínum. Sæktu Commons-appið núna: %1$s - Deila forriti með… + Deila forriti með... Upplýsingar í mynd Engir flokkar fundust Engar myndlýsingar fundust diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index e9aa8934ee..f408638708 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -46,38 +46,31 @@ Foto del giorno %1$d file in caricamento - %1$d file in caricamento %1$d file in caricamento (%1$d) - (%1$d) (%1$d) Avvio del caricamento Elaborando %d caricamento - Elaborando %d caricamenti Elaborando %d caricamenti %d caricamento - %d caricamenti %d caricamenti Questa immagine sarà rilasciata in base alla licenza %1$s - Queste immagini saranno rilasciate in base alla licenza %1$s Queste immagini saranno rilasciate in base alla licenza %1$s %1$d caricamento - %1$d caricamenti %1$d caricamenti Ricezione di contenuti condivisi. L\'elaborazione dell\'immagine potrebbe richiedere del tempo a seconda delle dimensioni dell\'immagine e del dispositivo - Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Ricezione di contenuti condivisi. L\'elaborazione delle immagini potrebbe richiedere del tempo a seconda delle dimensioni delle immagini e del dispositivo Esplora @@ -523,7 +516,7 @@ Accesso alla posizione multimediale negato Potremmo non essere in grado di ottenere automaticamente i dati sulla posizione dalle immagini caricate. Si prega di aggiungere la posizione appropriata per ciascuna immagine prima di inviarla Carica foto su Wikimedia Commons direttamente dal tuo telefono. Scarica subito l\'app Commons: %1$s - Condividi applicazione tramite… + Condividi applicazione tramite... Informazioni sull\'immagine Nessuna categoria trovata Nessuna definizione trovata @@ -550,7 +543,6 @@ Successo Categoria %1$s aggiunta. - Categorie %1$s aggiunte. Categorie %1$s aggiunte. Non è stato possibile aggiungere le categorie. @@ -583,7 +575,7 @@ Esiste Necessita della fotografia Tipo di luogo: - Ponte, museo, albergo, ecc… + Ponte, museo, albergo, ecc... Si è verificato un errore durante l\'accesso. Devi reimpostare la password! MEDIA CLASSI FIGLIE @@ -596,7 +588,7 @@ Aggiungi ai preferiti Qualcosa è andato storto. Non è stato possibile impostare lo sfondo schermo Imposta come sfondo - Impostazione di sfondo in corso… + Impostazione di sfondo in corso... Segui sistema Scuro Chiaro @@ -766,7 +758,6 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata - %d immagini selezionate %d immagini selezionate Questo posto non ha ancora una foto, scattane una! diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 4b8c51f6c1..0b512102b4 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -45,37 +45,44 @@ מועלה קובץ אחד מועלים %1$d קבצים + מועלים %1$d קבצים מועלים %1$d קבצים (%1$d) (%1$d) + (%1$d) (%1$d) ההעלאות מתחילות עיבוד העלאה עיבוד d% העלאות + עיבוד d% העלאות עיבוד d% העלאות העלאה אחת %d העלאות + %d העלאות %d העלאות התמונה הזאת תפורסם ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s + התמונות האלה תפורסמנה ברישיון %1$s התמונות האלה תפורסמנה ברישיון %1$s העלאה אחת %1$d העלאות + %1$d העלאות %1$d העלאות מתקבל תוכן שיתופי. עיבוד התמונה עשוי לארוך זמן מה כתלות בגודל התמונה והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך + מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך מתקבל תוכן שיתופי. עיבוד התמונות עשוי לארוך זמן מה כתלות בגודל התמונות והמכשיר שלך לחקור @@ -94,9 +101,9 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין… + נא להמתין... עדכון כיתובים ותיאורים - נא להמתין… + נא להמתין... הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -206,7 +213,7 @@ כן! מידע נוסף קטגוריות - בטעינה… + בטעינה... לא נבחר דבר אין כיתוב אין תיאור @@ -501,7 +508,7 @@ הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין… + נא להמתין... תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -523,7 +530,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך… + שיתוף היישום דרך... פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -551,6 +558,7 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. + נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -560,6 +568,7 @@ נוסף מוצג %1$s נוספו המוצגים %1$s + נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -602,7 +611,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין… + הגדרת טפט. נא להמתין... מערכת מעקב כהה בהירה @@ -662,7 +671,7 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה… + ביטול ההעלאה... ביטול ההעלאה הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. @@ -792,6 +801,7 @@ נבחרה תמונה אחת נבחרו שתי תמונות + נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -805,7 +815,7 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות… + ביטול כל ההעלאות... העלאות ממתינות נכשלו diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f60bb30ddc..f20b986f82 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -44,6 +44,7 @@ %1$d 件のファイルをアップロード中 + (%1$d) (%1$d) アップロードを開始中です @@ -54,12 +55,14 @@ %d 件のアップロード + この画像は%1$sライセンスのもとにアップロードされます これらの画像は%1$sライセンスのもとにアップロードされます %1$d 件のアップロード + 共有コンテンツを受信中です。 この画像の投稿の処理には、サイズやご使用の機器により時間がかかる事があります 共有コンテンツの受信中です。投稿画像の処理には、サイズやご使用の機器により時間がかかる事があります 探索 @@ -557,7 +560,7 @@ ブックマークに追加 問題が発生しました。壁紙を設定できませんでした。 壁紙として設定 - 壁紙を設定中。お待ちください… + 壁紙を設定中。お待ちください... システムのまま ダーク ライト diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index eb90e4a23c..40eb016295 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -47,8 +47,8 @@ Qqen Tettuḍ awal uffir? Jerred - Tuqqna… - Rǧu… + Tuqqna... + Rǧu... Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. @@ -100,7 +100,7 @@ Azen tikti (s yimayl) Ulac amsaɣ n yimayl ibedden Taggayin yettwasqedcenmelmi kan - Araǧu n umtawi amezwaru… + Araǧu n umtawi amezwaru... Ur tsuliḍ ara yakan tiwlafin. Ɛref̣ tikelt-nniḍen Sefsex @@ -130,7 +130,7 @@ Tɣileḍ igarrez? Ih! Taggayin - Asali… + Asali... Ula d yiwet ur tettwafren Ulac aglam Turagt tarussint diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 3703d373fb..aa7ae98e79 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -43,22 +43,28 @@ 검색 뷰 오늘의 이미지 + %1$d개의 파일을 올리는 중 %1$d개의 파일을 올리는 중 + (%1$d) (%1$d) 파일 올리기 + %1$d장의 업로드를 처리하는 중입니다 %1$d장의 업로드를 처리하는 중입니다 + %d개 업로드 %d개 업로드 + 이 그림은 %1$s에 따라 사용이 허가됩니다 이 그림은 %1$s에 따라 사용이 허가됩니다 + %1$d개 업로드 %1$d개 업로드 찾아보기 @@ -79,7 +85,7 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오… + 기다려 주십시오... 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -278,6 +284,7 @@ 위키텍스트를 클립보드에 복사했습니다 주변이 제대로 작동되지 않을 수 있습니다. 위치를 사용할 수 없습니다. 주변 장소의 목록을 표시하기 위한 권한이 필요합니다. + 주변 장소의 이미지 목록을 표시하기 위한 권한이 필요합니다 방향 위키데이터 위키백과 @@ -433,6 +440,7 @@ 완료 감사 표현 보내기: 성공 감사 표현 보내기: 실패 + 이것이 저작권 규정을 준수하고 있습니까? 알맞게 분류됐습니까? 기여자에게 감사를 표하시겠습니까? 앗, 분류가 달리지 않은 것 같습니다! @@ -447,10 +455,11 @@ 이미지가 올려지지 않음 읽지 않은 알림이 없습니다 읽은 알림이 없습니다 + 이메일의 받은 편지함을 확인하세요 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오… + 기다려 주십시오... 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -465,7 +474,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유… + 앱 공유... 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -493,6 +502,7 @@ 성공 설명이 추가되었습니다. 캡션이 추가되었습니다. + 좌표를 추가하지 못했습니다. 설명을 추가하지 못했습니다. 캡션을 추가하지 못했습니다. 이미지 좌표가 업데이트되지 않았습니다 @@ -523,7 +533,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오… + 배경화면을 설정 중입니다. 기다려 주십시오... 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 @@ -543,6 +553,7 @@ 일시 정지 계속하기 일시 중단됨 + 더 보기 책갈피 리더보드 순위: @@ -638,6 +649,9 @@ 전체 화면 선택 모드에 오신 것을 환영합니다 두 손가락으로 확대 / 축소하세요. 다음 방향으로 길고 재빠르게 넘겨보세요. \n- 왼쪽/오른쪽: 이전/다음으로 이동 \n- 위쪽: 선택\n- 아래쪽: 비업로드용으로 표시 + 스토리지 접근이 거부됨 + 이 항목을 공유할 수 없습니다 + 기능에 대한 권한이 필요합니다 유용한 설명을 추가하는 법 알아보기 유용한 캡션을 추가하는 법 알아보기 업적 보기 @@ -651,6 +665,7 @@ 작성자에게 감사 표시하기 작성자에게 감사를 표하던 도중에 오류가 발생하였습니다. 로그인 세션 만료. 다시 로그인해 주십시오. + GPX 파일을 열 수 있는 응용 프로그램이 없습니다 파일이 성공적으로 저장되었습니다 GPX 파일을 여시겠습니까? KML 파일을 여시겠습니까? @@ -658,8 +673,20 @@ GPX 파일을 저장하지 못했습니다. KML 파일을 저장 중 GPX 파일을 저장 중 + + %d개 이미지 선택됨 + 다중 업로드에 대한 참고사항 이 항목에 관한 문제를 위키데이터에 보고하기 + 의견을 입력해 주십시오 토론 기타 문제 또는 정보 (아래에 설명해 주십시오) + 모든 업로드를 취소하는 중... + 업로드 + 보류 중 + 실패 + 장소 데이터를 불러오지 못했습니다 + 이 장소에 아직 사진이 없습니다. 사진을 찍어보세요! + 이 장소에 이미 사진이 있습니다. + 지금 이 장소에 사진이 있는지 확인 중입니다. diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index be63e9db5b..55fa4ac359 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -143,7 +143,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады… + Биринчи синхронизацияны сакълаб турады... Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -227,7 +227,7 @@ Ызына ал Ач Джаб - Баш бет + Тамал бет Джюкле Джуўукъда Юсюнден @@ -498,7 +498,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле… + Къошакъны буну бла юлюшле... Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +575,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +633,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады… - Джюклениу туракъланады… - Джюклениу ызына алынады… + Джюклениу андан ары бардырылады... + Джюклениу туракъланады... + Джюклениу ызына алынады... Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index d9d5b65b91..506e9e4b47 100644 --- a/app/src/main/res/values-ku/strings.xml +++ b/app/src/main/res/values-ku/strings.xml @@ -70,8 +70,8 @@ Te şîfreya xwe ji bîr kir? Xwe tomar bike Têdikeve - Ji kerema xwe piçek bisekine … - Xêra xwe hinek bisekine… + Ji kerema xwe piçek bisekine ... + Xêra xwe hinek bisekine... Têketin bi ser ket! Têketin bi ser neket! Dosye nehat dîtin. Ji kerema xwe re dosyeyek din biceribîne. @@ -183,7 +183,7 @@ Wêneyên Barkirî Wêneyê din Belê, çima na - Ji kerema xwe piçek bisekine … + Ji kerema xwe piçek bisekine ... Wêne tevlî Wîkîpediyayê bike Tu dixwazî vê wêneyê tevlî gotara Wîkîpediyayê ya bi zimanê %1$s bikî? Pişrast bike diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml index ab657b354a..8112afea61 100644 --- a/app/src/main/res/values-kum/strings.xml +++ b/app/src/main/res/values-kum/strings.xml @@ -49,7 +49,7 @@ Юклев уьлгю: Дюр! Категориялар - Юклев… + Юклев... Бир зат сайланмагъан Тасвири ёкъ Пикирлешивлер ёкъ diff --git a/app/src/main/res/values-kus/strings.xml b/app/src/main/res/values-kus/strings.xml index 02abd4ea10..99fb8c1f74 100644 --- a/app/src/main/res/values-kus/strings.xml +++ b/app/src/main/res/values-kus/strings.xml @@ -62,9 +62,9 @@ Fʋ tami fʋ paaswɛɛtɛ? Yɔ\'ɔgin kpɛn\' Kpɛn\'ɛdnɛ - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Maligim maal pian\'azut nɛ pa\'alʋg nam - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Kpɛn\'ɛb nyaŋya Kpɛn\'ɛb gʋ\'ʋŋya M Pʋ nyɛ faal la. M bɛlimnɛ tiakim faal si\'a. @@ -169,7 +169,7 @@ Ɛɛn! Labaya bɛdigʋ Buudi kɔn\'ɔb-kɔn\'ɔb - Bɛ tʋʋma ni… + Bɛ tʋʋma ni... Pʋ gaŋ si\'ela Pian\'azug kae Pa\'alʋg kae @@ -400,7 +400,7 @@ Gɔsim dinɛ ka fʋ karim sa Gɔsim dinɛ ka fʋ nam pʋ karim Daʋŋʋ kidig footonam la nɔkirin - M bɛlimnɛ gu\'usim… + M bɛlimnɛ gu\'usim... Footo banɛ ka fʋ kpɛn\'ɛsi dɔlis zin\'ibanɛ be yamma anɛ footo banɛ ka fʋ kpɛn\'ɛs ka di yinɛ fʋn nyɛ di map ni la. Yaam paas media banɛ bɛ tuon Yaaiya @@ -418,7 +418,7 @@ Serial Numbers Software Pʋ bas suor ye fʋ kpɛn\' midia zin\'iginɛ - Pʋdigim app la dɔlis… + Pʋdigim app la dɔlis... Footo labaar Pʋ paam buudinama Pʋ nyɛ nwɛnnɛm si\'aa. @@ -492,7 +492,7 @@ Ba zaŋi paas bookmarknamin Daʋŋsi\'a naam. Pʋ nyaŋi maal nibdaa footo la Maalimi fʋ nindaa footo la - Maanɛ nindaa footo. M bɛlimnɛ gu\'usim… + Maanɛ nindaa footo. M bɛlimnɛ gu\'usim... Dɔl sistɛm la Lik Nɛɛsim @@ -538,9 +538,9 @@ Bas suor ye di tʋm saŋa bi\'ela! Atʋm bi\'ela zi\'esim Footo sʋma - Lɛm pin\'in kpɛn\'ɛsʋg… - Gu\'om kpɛn\'ɛsʋg… - Basid kpɛn\'ɛsʋg… + Lɛm pin\'in kpɛn\'ɛsʋg... + Gu\'om kpɛn\'ɛsʋg... + Basid kpɛn\'ɛsʋg... Basim kpɛn\'ɛsʋg Bas suor ye di tʋm saŋa bi\'ela. Nwɛnnɛm nam diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 8b2ab6b955..2eb2fcf2f5 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -21,6 +21,7 @@ %1$d файл жүктөлүүдө + Азырынча жүктөөлөр жок 1 жүктөө %1$d жүктөө @@ -136,7 +137,7 @@ Жүктөөнү жокко чыгаруу Артка баскычын колдонуу менен бул жүктөө жокко чыгарылат жана сиз ийгиликти жоготосуз Жүктөөнү улантуу - Күтө туруңуз… + Күтө туруңуз... Аталыш Сыпаттама Элементтер diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index d99e269ab1..2ef8f9e031 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -61,7 +61,7 @@ Aloggen Waart wgl. … Beschrëftungen a Beschreiwungen aktualiséieren - Waart wgl. … + Waart wgl. ... Umeldung huet geklappt! D\'Aloggen huet net funktionéiert! Fichier net fonnt. Probéiert wgl. en anere Fichier. @@ -86,6 +86,7 @@ Nobäi Meng eropgeluede Fichieren Deelen + Fichierssäit weisen Beschrëftung (obligatoresch) Gitt wgl. eng Beschrëftung fir dëse Fichier un Beschreiwung @@ -93,6 +94,7 @@ Aloggen huet net funktionéiert – Problemer mam Reseau Ze dacks ouni Succès probéiert. Probéiert wgl. an e puer Minutten nach eng Kéier. Pardon, dëse Benotzer ass op Commons gespaart + Dir musst de Code vun Ärer Zwee-Facteur-Authentifizéierung uginn. Aloggen huet net funktionéiert Eroplueden Gitt dëser Biller een Numm @@ -349,7 +351,7 @@ Déi geliese weisen Déi net geliese weisen Feeler beim Eraussiche vun de Biller - Waart wgl. … + Waart wgl. ... Kopéiert Beispiller vu gudde Biller fir op Commons eropzelueden Beispiller fir Biller, déi een net eropluede sollt @@ -361,7 +363,7 @@ Seriennummeren Software Luet Fotoen direkt vun Ärem Handy op Wikimedia Commons erop. Luet d\'Commons-App elo erof: %1$s - App deelen iwwer… + App deelen iwwer... Bildinformatiounen Keng Kategorie fonnt. Eroplueden ofgebrach @@ -411,7 +413,7 @@ Bei d\'Lieszeechen derbäigesat Et ass Eppes schif gaangen. D\'Hannergrondbild konnt net agestallt ginn Als Hannergrondbild festleeën - Hannergrondbild gëtt agestallt. Waart wgl… + Hannergrondbild gëtt agestallt. Waart wgl... System suivéieren Däischter Hell @@ -454,7 +456,7 @@ Limitéierte Verbindungsmodus Qualitéitsbiller Qualitéitsbiller sinn Diagrammen oder Fotoen, déi gewësse Qualitéitscritèren erfëllen (déi haaptsächlech vun technescher Natur sinn) a wäertvoll fir Wikimedia-Projete sinn. - Eropluede gëtt ofgebrach…. + Eropluede gëtt ofgebrach.... Eroplueden ofbriechen Kategoriesäit weisen Sprooch vum Interface vum Benotzer vun der App diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index 1720bfbcb4..f477ed8f0b 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -33,8 +33,8 @@ Melj dich aan Wachwaord vergaete? Teiken dich in - Aan \'nt melje… - Wach estebleef… + Aan \'nt melje... + Wach estebleef... Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. @@ -88,7 +88,7 @@ Sjik feedback (mitten e-mail) Geine e-mailcliënt geïnstalleerd Recèntelik gebroekde categorieje - Oppe ieëste synchronisatie \'nt wachte… + Oppe ieëste synchronisatie \'nt wachte... Doe höbs nag gein plaetjes geüpload. Perbeer oppernuuj Braek aaf @@ -127,7 +127,7 @@ Versteis se \'t? Jao! Categorieje - \'nt laje… + \'nt laje... Geine gekaoze Gein besjrieving Ónbekande licentie diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cb7bebe41f..26a9bc7f77 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -33,27 +33,20 @@ Dienos nuotrauka %1$d keliamas failas - %1$d keliami failai - %1$d failų keliamas %1$d keliami failai - %1$d įkėlimas - %1$d įkėlimai - %1$d įkėlimų + \@string/contributions_subtitle_zero + 1 įkėlimas Įkėlimai pradedami Pradedamas %1$d įkėlimas - Pradedami %1$d įkėlimai - Pradedami %1$d įkėlimų Pradedami %1$d įkėlimai %1$d įkėlimas - %1$d įkėlimai - %1$d įkėlimų %1$d įkėlimai Šio paveikslėlio licencija bus %1$s @@ -75,7 +68,7 @@ Jungiamasi Prašome palaukti… Antraštės ir aprašymai atnaujinami - Prašome palaukti… + Prašome palaukti... Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. @@ -179,7 +172,7 @@ Taip! Daugiau informacijos Kategorijos - Kraunasi… + Kraunasi... Niekas nepasirinkta Nėra antraštės Nėra aprašymo @@ -472,7 +465,7 @@ Žiūrėti perskaitytus Žiūrėti neperskaitytus Renkant vaizdus įvyko klaida - Prašome palaukti… + Prašome palaukti... Rinktinės nuotraukos yra aukštos kvalifikacijos fotografų ir iliustratorių vaizdai, kuriuos Vikiteka bendruomenė pasirinko kaip svetainėje aukščiausios kokybės. Vaizdai, įkelti per Netoliese esančias vietas, yra vaizdai, kurie įkeliami atrandant vietas žemėlapyje. Ši funkcija leidžia redaktoriams siųsti padėkos pranešimą naudotojams, kurie atlieka naudingus pakeitimus, naudojant nedidelę padėkos nuorodą istorijos puslapyje arba skirtumų puslapyje. @@ -493,7 +486,7 @@ Prieiga prie medijos vietos uždrausta Gali būti, kad negalėsime automatiškai gauti vietos duomenų iš jūsų įkeltų nuotraukų. Prieš pateikdami kiekvienai nuotraukai pridėkite tinkamą vietą Įkelkite nuotraukas į Vikiteką tiesiai iš savo telefono. Atsisiųskite Vikitekos programėlę dabar: %1$s - Dalintis programą per … + Dalintis programą per ... Vaizdo informacija Kategorijų nerasta Vaizdų nerasta @@ -753,7 +746,7 @@ Kita problema arba informacija (paaiškinkite toliau). Jūsų atsiliepimai bus paskelbti šiame viki puslapyje: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile App/Feedback</a> Ar tikrai norite atšaukti visus įkėlimus? - Atšaukiami visi įkėlimai… + Atšaukiami visi įkėlimai... Įkėlimai Laukiama Nepavyko diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 9038eec9de..7a6d9e3628 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -21,7 +21,7 @@ Reģistrēties Pieslēdzas Lūdzu, uzgaidiet… - Lūdzu, uzgaidi… + Lūdzu, uzgaidi... Ieiešana veiksmīga Pieteikšanās neizdevās. Autentifikācija neizdevās! @@ -163,7 +163,7 @@ Nākamais attēls Skatīt arhivētos Skatīt nelasītos - Lūdzu, uzgaidiet… + Lūdzu, uzgaidiet... Izlaist šo attēlu Autors Autortiesības diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index c496505ae9..916f4f4202 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -4,7 +4,7 @@ * Violetova * Vlad5250 --> - + Ризницата на Фејсбук Изворен код на Ризницата на Github Лого на Ризницата @@ -52,7 +52,7 @@ %1$d подигања - Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред + Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликата и вашиот уред Добивам споделена содржина. Обработката може да потрае, зависно од големината на сликите и вашиот уред Истражи @@ -73,7 +73,7 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте… + Почекајте... Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -479,7 +479,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте… + Почекајте... Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +501,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку… + Сподели преку... Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -780,7 +780,7 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања… + Ги откажувам сите подигања... Подигања Во исчекување Неуспешно diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index 0d8e029a4c..de888dcbc3 100644 --- a/app/src/main/res/values-mni/strings.xml +++ b/app/src/main/res/values-mni/strings.xml @@ -18,7 +18,7 @@ ꯈꯨꯠꯌꯦꯛ ꯄꯤꯈꯠꯂꯨ ꯃꯅꯨꯡ ꯆꯪꯁꯤꯟꯂꯤ ꯉꯥꯏꯍꯥꯛ ꯉꯥꯏꯕꯤꯌꯨ - ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ… + ꯉꯥꯏꯍꯥꯛꯇꯪ ꯉꯥꯏꯕꯤꯈꯣ... ꯃꯥꯏꯄꯥꯛꯅꯥ ꯆꯪꯁꯤꯜꯂꯦ ꯫ ꯆꯪꯁꯤꯟꯕ ꯃꯥꯏꯄꯥꯛꯇꯔꯦ! ꯐꯥꯏꯜ ꯊꯤꯕꯥ ꯐꯪꯗꯔꯦ ꯫ ꯆꯥꯟꯕꯤꯗꯨꯅꯥ ꯑꯇꯣꯞꯄ ꯑꯃꯥ ꯇꯧꯕꯤꯔꯣ ꯫ @@ -59,7 +59,7 @@ ꯍꯣꯏ! ꯑꯍꯦꯟꯕ ꯋꯥꯔꯣꯜ ꯃꯆꯥꯈꯥꯏꯕꯁꯤꯡ - ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ….. + ꯆꯤꯡꯂꯤ/ꯎꯪꯂꯤ..... ꯑꯃꯠꯇ ꯈꯟꯗꯦ ꯑꯀꯨꯞꯄ ꯃꯔꯣꯜ ꯌꯥꯎꯗꯦ ꯈꯟꯅ-ꯅꯩꯅꯕ ꯂꯩꯇꯦ diff --git a/app/src/main/res/values-mnw/strings.xml b/app/src/main/res/values-mnw/strings.xml index 27a76b0a75..a6c18bca30 100644 --- a/app/src/main/res/values-mnw/strings.xml +++ b/app/src/main/res/values-mnw/strings.xml @@ -45,7 +45,7 @@ ဝိုတ်စ မအက္ခရ်ပၞုက် ပတိုန် စၟတ်သမ္တီ လုပ်လံက်အေန် ဒၟံင် - ပဂုန်တုဲ မင်မွဲလစုတ်… + ပဂုန်တုဲ မင်မွဲလစုတ်... လုက်အေန် အာစိုပ်ဒတုဲ! လံက်အေန် လီုလာ်! ဝှာင် ဟွံဂွံဆဵု၊ ပဂုန်တုဲ ဂၠာဲ ဝှာင်တၞဟ်။ @@ -148,7 +148,7 @@ ယွံ! ဆက်လဴ ပရူတင်ဂၞင် ကဏ္ဍဂမၠိုင် - ပတိုန်ဒၟံင်… + ပတိုန်ဒၟံင်... ဟွံမဲကဵု ပရေၚ်ရုဲစှ် ဟွံမဲကဵု က္ဍိုပ်လိက် ဟွံမဲကဵု ဗမံက်ထ္ၜး diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index 9655945855..546b43f4fa 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -17,6 +17,7 @@ %1$d संचिका अपभारीत होत आहे + अद्याप अपभारणे नाहीत एक अपभारण %1$d अपभारणे @@ -93,7 +94,7 @@ प्रतिसाद पाठवा (विपत्राद्वारे) कोणतेही ईमेल क्लायंट स्थापित नाहीत अलीकडे वापरलेले वर्ग - प्रथम संकालनाची प्रतीक्षा करीत आहे … + प्रथम संकालनाची प्रतीक्षा करीत आहे ... आपण अद्याप काहीच चित्रे अपभारीत केली नाहीत. पुन्हा प्रयत्न करा रद्द करा diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index e5dd0f3beb..1fce0c0daf 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -19,16 +19,20 @@ အားလုံး ယနေ့အတွက် အထူးဓာတ်ပုံ + ဖိုင် %1$d ခု တင်နေသည် ဖိုင် %1$d ခု တင်နေသည် အပ်ပလုဒ်များ စတင်ခြင်း + %1$d ခု တင်ထားသည် %1$d ခု တင်ထားသည် + ဤရုပ်ပုံသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် ဤရုပ်ပုံများသည် %1$ အောက်တွင် လိုင်စင်သတ်မှတ်ထးပါမည် + %1$d အက်ပလုပ် %1$d အက်ပလုပ်များ ရှာဖွေစူးစမ်းပါ @@ -45,9 +49,9 @@ အကောင့်ဝင်ရန် စကားဝှက် မေ့နေပါသလား မှတ်ပုံတင်ရန် - လော့ဂ်အင် ဝင်ရောက်နေသည်… - ခေတ္တစောင့်ပါ… - ကျေးဇူးပြု၍ ခဏစောင့်ပါ… + လော့ဂ်အင် ဝင်ရောက်နေသည်... + ခေတ္တစောင့်ပါ... + ကျေးဇူးပြု၍ ခဏစောင့်ပါ... လော့အင် အောင်မြင်သည် လော့အင် မအောင်မြင်ပါ ဖိုင်မတွေ့ပါ၊ အခြးဖိုင်တစ်ခု စမ်းကြည့်ပါ။ @@ -129,7 +133,7 @@ ဟုတ်ကဲ့ သတင်းအချက်အလက် ပို၍ ကဏ္ဍများ - ဝန်ဆွဲတင်နေသည်… + ဝန်ဆွဲတင်နေသည်... ဘာမှရွေးချယ်မထားပါ ပုံစာ မရှိ ဖော်ပြချက် မရှိ @@ -311,7 +315,7 @@ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ မဖတ်ရသေးသော အသိပေးချက်များ မရှိပါ ရုပ်ပုံများကိုရွေးနေစဉ် အမှားဖြစ်ပွားခဲ့ပါသည် - ကျေးဇူးပြု၍ ခဏစောင့်ပါ… + ကျေးဇူးပြု၍ ခဏစောင့်ပါ... နမူနာရုပ်ပုံများ အက်ပလုပ်တင်ရန် မဟုတ်ပါ ဤရုပ်ပုံအား ကျော်သွားမည် ဒေါင်းလုဒ် မအောင်မြင်ပါ။ ပြင်ပသိုလှောင်မှုခွင့်ပြုချက်မရှိဘဲ ဖိုင်ဒေါင်းလုဒ်မဆွဲနိုင်ပါ။ diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3b4bf30dc9..de59e28de0 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -522,7 +522,7 @@ Toegang tot medialocatie geweigerd Het is mogelijk dat we niet automatisch locatiegegevens kunnen verkrijgen van foto\'s die u uploadt. Voeg de locatie bij elke foto toe voordat u die upload Upload foto\'s rechtstreeks vanaf uw telefoon naar Wikimedia Commons. Download de Commons-app nu: %1$s - App delen via… + App delen via... Afbeeldingsinfo Geen categorieën gevonden Geen beschrijvingen gevonden @@ -599,7 +599,7 @@ Als bladwijzer toegevoegd Er is iets fout gegaan. Kan de achtergrond niet instellen Instellen als achtergrond - Wordt ingesteld als achtergrond. Een ogenblik geduld… + Wordt ingesteld als achtergrond. Een ogenblik geduld... Systeem volgen Donker Licht @@ -659,7 +659,7 @@ Kwaliteitsafbeeldingen zijn diagrammen of foto\'s die voldoen aan bepaalde kwaliteitsnormen (die meestal technisch van aard zijn) en waardevol zijn voor Wikimedia-projecten Uploaden hervatten… Uploaden onderbreken… - Uploaden wordt geannuleerd… + Uploaden wordt geannuleerd... Uploaden Annuleren U hebt de beperkte verbindingsmodus ingeschakeld. Alle uploads worden gepauzeerd en worden hervat zodra u deze modus uitschakelt. Beperkte verbindingsmodus is ingeschakeld. @@ -806,4 +806,7 @@ In behandeling Mislukt Plaatsgegevens konden niet geladen worden + Er is nog geen foto van deze plek, maak er eentje! + Er is al een foto van deze plek. + We controleren nu of er een foto van deze plek is. diff --git a/app/src/main/res/values-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 62e01d4d56..7e11ea03a3 100644 --- a/app/src/main/res/values-nqo/strings.xml +++ b/app/src/main/res/values-nqo/strings.xml @@ -51,9 +51,9 @@ ߌ ߓߘߊ߫ ߢߌ߬ߣߊ߬ ߌ ߟߊ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߊ߫؟ ߖߊ߬ߕߋ߬ߘߊ ߟߊߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߦߋ߫ ߛߋ߲߬ߠߊ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߝߍ߬ߛߓߍߟߌ ߣߌ߫ ߞߊ߲߬ߛߓߍߟߌ ߟߊߞߎߘߦߊ ߦߴߌ ߘߐ߫ - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫߹ ߞߐߕߐ߮ ߡߊ߫ ߛߐ߬ߘߐ߲߬. ߘߏ߫ ߜߘߍ߫ ߡߊߝߍߣߍ߲߫ ߖߊ߰ߣߌ߲߫. @@ -69,7 +69,7 @@ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߊ߬ ߛߐ߲߬ߞߌ߲߫ ߞߵߊ߬ ߦߋ߫ ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲߬ ߞߐ߯ߟߕߊ ߟߎ߬ - ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫… + ߘߞߐ߬ߣߐ߲߬ߠߌ߲ ߘߐ߫... ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫ %1$d%% ߓߘߊ߫ ߘߝߊ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߦߋ߫ ߛߋ߲߬ߠߊ߫ @@ -148,7 +148,7 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -408,7 +408,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +418,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +486,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +533,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index eab67e0766..c097898e9f 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -81,7 +81,7 @@ Mandar vòstres comentaris (per corrièl) Cap de client de corrièl pas installat Categorias utilizadas recentament - Espèra de primièra sincronizacion… + Espèra de primièra sincronizacion... Avètz pas encara telecargat cap de fòto. Tornar ensajar Anullar diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index d0ee733960..8c64900a56 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -8,7 +8,7 @@ * Sony dandiwal * ਗੁਰਪ੍ਰੀਤ ਹੁੰਦਲ --> - + ਕਾਮਨਜ਼ ਮਾਰਕਾ ਇੱਕ ਹੋਰ ਵੇਰਵਾ ਸ਼ਾਮਲ ਕਰੋ ਨਵਾਂ ਯੋਗਦਾਨ ਸ਼ਾਮਲ ਕਰੋ @@ -17,10 +17,11 @@ ਸਾਰੇ ਦਿਨ ਦੀ ਤਸਵੀਰ - ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ + ੧ ਫ਼ਾਈਲ ਚੜ੍ਹਾਈ ਜਾ ਰਹੀ ਹੈ %1$d ਫ਼ਾਈਲਾਂ ਚੜ੍ਹਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ + \@string/contributions_subtitle_zero %1$d upload %1$d ਅੱਪਲੋਡ @@ -29,7 +30,7 @@ %1$d ਸ਼ੁਰੂ ਹੋ ਰਹੇ ਹਨ - %1$d ਅੱਪਲੋਡ + &d ਅੱਪਲੋਡ %1$d ਅੱਪਲੋਡਾਂ ਇਹ ਤਸਵੀਰ ਦਾ %1$s ਹੇਠ ਲਸੰਸ ਜਾਰੀ ਕੀਤੀ ਜਾਵੇਗਾ @@ -44,7 +45,7 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -128,7 +129,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ… + ਲੱਦ ਰਿਹਾ ਹੈ... ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -200,7 +201,7 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ ਲਿਖਤ ਛਾਪੋ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 09132f40e1..dcd8ea284a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -508,7 +508,7 @@ Zobacz przeczytane Wyświetl nieprzeczytane Wystąpił błąd podczas pobierania zdjęć - Proszę czekać… + Proszę czekać... Polecane zdjęcia to zdjęcia wysoko wykwalifikowanych fotografów i ilustratorów, które społeczność Wikimedia Commons wybrała jako jedne z najwyższych jakości na stronie. Obrazy przesłane przez Pobliskie miejsca to obrazy, które są przesyłane przez odkrywanie miejsc na mapie. Ta funkcja umożliwia redaktorom wysyłanie powiadomień z podziękowaniem do użytkowników, którzy dokonują przydatnych zmian - za pomocą małego linku z podziękowaniem na stronie historii lub na stronie diff. diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index b7449e9571..bfbd64413b 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -477,7 +477,7 @@ Vëdde lòn ch\'a l\'é stàit lesù Vëdde lòn ch\'a l\'é ancor nen ëstàit lesù A-i é staje n\'eror an selessionand le plance - Ch\'a l\'abia passiensa… + Ch\'a l\'abia passiensa... Le fòto an evidensa a son ëd plance fàite da dij fotògraf e ilustrator motobin àbij che la comunità ëd Wikipedia Commons a l\'ha sernù tra cole ëd qualità pi àuta an sël sit. Le plance carià dai pòst ëd prossimità a son le plance carià con la dëscuverta dij pòst an sla carta. Costa fonsionalità a përmet ai contributor ëd mandé na notìfica d\'aringrassiament a j\'utent ch\'a fan dle modìfiche ùtij - an dovrand na cita liura d\'aringrassiament an sla pàgina dla stòria o cola dle diferense. @@ -499,7 +499,7 @@ Acess a la locassion dël mojen arfudà I podoma pa oten-e an automàtich ij dàit ëd localisassion dle plance che chiel a caria. Për piasì, ch\'a giontà la posission apropià për tute le plance prima ëd mandeje Ch\'a caria dle fòto su Wikimedia Commons diretaman da sò teléfon. Ch\'a dëscaria l\'aplicassion Commons adess: %1$s - Partagé l\'aplicassion via… + Partagé l\'aplicassion via... Anformassion an sla plancia Gnun-e categorìe trovà Gnun-e descrission trovà @@ -776,9 +776,12 @@ Àutr problema o anformassion (për piasì, ch\'a spiega sì-sota). Ij sò sugeriment a saran giontà a coste pàgine wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> É-lo sigur ëd vorèj anulé tuti ij cariament? - Anulament ëd tuti ij cariament… + Anulament ëd tuti ij cariament... Cariament An atèisa Falì Impossìbil carié ij dàit dël pòst + Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! + Ës pòst a l\'ha già dle fòto. + An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 461cb6b1d8..4f17da26f7 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -65,7 +65,7 @@ CC BY 3.0 هو وېشنيزې - رابرسېرېږي… + رابرسېرېږي... هېڅ هم نه دی ټاکل شوی څرگندونه نشته نامعلوم جواز diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index b0dd3b016a..3779a8a516 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -28,7 +28,7 @@ * Tuliouel * YuriNikolai --> - + Página do Commons no Facebook Código fonte do Commons no Github Logotipo do Commons @@ -51,39 +51,32 @@ Estado do local Imagem do Dia - carregando arquivo - carregando %1$d arquivos + carregando arquivo carregando %1$d arquivos (%1$d) - (%1$d) (%1$d) Iniciando carregamentos Processando %d carregamento - Processando %d carregamentos Processando %d carregamentos %d carregamento - %d carregamentos %d carregamentos Esta imagem será licenciada sob %1$s - Estas imagens serão licenciadas sob %1$s Estas imagens serão licenciadas sob %1$s %1$d carregamento - %1$d carregamentos %1$d carregamentos - Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo - Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo + Recebendo conteúdo compartilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da imagem e do seu dispositivo Recebendo conteúdo compartilhado. O processamento das imagens pode levar algum tempo, dependendo do tamanho das imagens e do dispositivo Explorar @@ -102,9 +95,9 @@ Esqueceu a senha? Cadastre-se Efetuar login - Por favor, aguarde… + Por favor, aguarde... Atualizando legendas e descrições - Por favor, aguarde… + Por favor, aguarde... Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. @@ -168,7 +161,7 @@ Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"%1$s\">publicação no GitHub</a> para informar erros e sugestões. - Política de privacidade + Politica de privacidade Créditos Sobre Enviar comentários (por e-mail) @@ -255,7 +248,7 @@ Ponte de Arco-Íris Tulipa Bem-vindo à Wikipédia - Direitos de autor são bem-vindo + Direitos de autor são bem vindo Ópera de Sydney Cancelar Abrir @@ -313,7 +306,7 @@ Commons Avalie-nos Perguntas frequentes - Guia de usuário + Guia de usuario Pular Tutorial A Internet não está disponível Erro ao tentar obter as notificações @@ -528,7 +521,7 @@ Acesso à localização da mídia negado É possível que não possamos obter automaticamente os dados de localização das imagens que você carregar. Por favor adicione a localização adequada para cada imagem antes de envia-las Carregue fotos na wiki Wikimedia Commons, diretamente do seu celular. Baixe o aolicativo Commons agora: %1$s - Compartilhar aplicativo via… + Compartilhar aplicativo via... Informação da imagem Nenhuma categoria encontrada Nenhuma representação encontrada @@ -555,7 +548,6 @@ Sucesso A categoria %1$s foi adicionada. - As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -564,7 +556,6 @@ Editar representações O elemento retratado %1$s está adicionado. - Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -776,7 +767,6 @@ Salvar arquivo GPX %d imagem selecinada - %d imagens selecionadas %d imagens selecionadas Escreva algo sobre o item %1$s. Isso será visivel publicamente. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 19a52d72ff..bad9dc5005 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -20,7 +20,7 @@ * Unamane * Vitorvicentevalente --> - + Página da wiki Commons no Facebook Código-fonte da wiki Commons no Github Logótipo da wiki Commons @@ -44,38 +44,31 @@ Imagem do Dia a carregar %1$d ficheiro - a carregar muitos %1$d ficheiros a carregar %1$d ficheiros (%1$d) - (%1$d) (%1$d) A iniciar carregamentos A processar %d carregamento - A processar %d carregamentos A processar %d carregamentos %d carregamento - %d carregamentos %d carregamentos Esta imagem será licenciada com a %1$s - Estas imagens serão licenciadas com a %1$s Estas imagens serão licenciadas com a %1$s %1$d carregamento - %1$d carregamentos %1$d carregamentos - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo - A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo + A receber conteúdo partilhado. O processamento da imagem pode demorar algum tempo, dependendo do tamanho da mesma e do seu dispositivo A receber conteúdo partilhado. O processamento das imagens pode demorar algum tempo, dependendo do tamanho das mesmas e do seu dispositivo Explorar @@ -163,8 +156,8 @@ Política de privacidade Créditos Sobre - Enviar comentários (por correio eletrónico) - Não foi instalado nenhum cliente de correio eletrónico + Enviar comentários (por correio eletrónico) + Não foi instalado nenhum cliente de correio eletrónico Categorias usadas recentemente A aguardar pela primeira sincronização… Não carregou ainda nenhuma foto. @@ -283,7 +276,7 @@ Gravar as fotografias tiradas com a câmara da aplicação no armazenamento do seu dispositivo Inicie sessão na sua conta Enviar ficheiro de registos - Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas + Enviar o ficheiro de registos aos programadores por correio eletrónico para ajudar a corrigir problemas da aplicação. Nota: os registos podem conter informações identificativas Não foi encontrado nenhum navegador da Internet para abrir o URL Erro! Não foi possível encontrar o URL Nomear para eliminação @@ -498,7 +491,7 @@ Ver lidas Ver não lidas Ocorreu um erro ao escolher imagens - Aguarde, por favor… + Aguarde, por favor... As fotografias destacadas são imagens de fotógrafos e ilustradores altamente qualificados, que a comunidade da wiki Wikimedia Commons escolheu como as de melhor qualidade do \'\'site\'\'. As imagens carregadas via \"Locais próximos\" são as imagens que são carregadas descobrindo locais do mapa. Esta funcionalidade permite que os editores enviem uma notificação de agradecimento aos utilizadores que fizerem edições úteis - usando uma pequena hiperligação de agradecimento na página do historial ou na de diferenças. @@ -520,7 +513,7 @@ Acesso à localização de multimédia negado Podemos não conseguir obter automaticamente os dados de localização das fotografias que carregar. Adicione a localização apropriada de cada fotografia antes de a enviar, por favor Carregue fotografias na wiki Wikimedia Commons, diretamente do seu telemóvel. Descarregue a aplicação Commons agora: %1$s - Partilhar aplicação por… + Partilhar aplicação por... Informação da imagem Não foi encontrada nenhuma categoria Não foi encontrada nenhuma representação @@ -547,7 +540,6 @@ Êxito A categoria %1$s foi adicionada. - As categorias %1$s foram adicionadas. As categorias %1$s foram adicionadas. Não foi possível adicionar categorias. @@ -556,7 +548,6 @@ Editar elementos retratados O elemento retratado %1$s está adicionado. - Os elementos retratados %1$s estão adicionados. Os elementos retratados %1$s estão adicionados. Não foi possível adicionar os elementos retratados. @@ -598,7 +589,7 @@ Adicionado aos marcadores Ocorreu um problema. Não foi possível definir a imagem de fundo Definir como imagem de fundo - A definir a imagem de fundo. Aguarde, por favor… + A definir a imagem de fundo. Aguarde, por favor... Seguir sistema Escuro Claro @@ -654,8 +645,8 @@ Modo de ligação limitada Imagens de qualidade As imagens de qualidade são diagramas ou fotografias que satisfazem certos padrões de qualidade (principalmente de natureza técnica) e são valiosos para projetos da Wikimedia - A retomar carregamento… - A pausar carregamento… + A retomar carregamento... + A pausar carregamento... A cancelar o carregamento… Cancelar carregamento Ativou o modo de ligação limitada. Todos os carregamentos foram colocados em pausa e serão retomados quando desativar este modo. @@ -718,7 +709,7 @@ Não foi encontrada nenhuma localização Que tal adicionar o local onde a imagem foi tirada?\nOs dados de localização ajudam os editores da wiki a encontrarem a sua fotografia, tornando-a muito mais útil.\nObrigado! Adicionar localização - Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. + Remova desta mensagem de correio todas as informações que não se sinta à vontade em partilhar publicamente, por favor. Adicionalmente, esteja consciente de que o seu endereço de correio eletrónico, com o qual está a fazer esta publicação, e o nome e imagem de perfil a ele associados, serão visíveis pelo público geral. Detalhes As realizações só estão disponíveis na versão de produção; consulte a documentação para programadores, por favor. A tabela de classificação só está disponível na versão prod. Consulte a documentação do desenvolvedor. @@ -769,7 +760,6 @@ Erro no envio de agradecimento ao autor. %d imagem selecionada - %d imagens selecionadas %d imagens selecionadas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index ad1d0b805f..0bcbc15506 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -28,8 +28,8 @@ %1$d de fișiere se încarcă + \@string/contributions_subtitle_zero (%1$d) - (%1$d) (%1$d) Pornirea încărcărilor @@ -74,8 +74,8 @@ V-ați uitat parola? Înregistrare Se conectează - Vă rugăm să așteptați … - Vă rugăm să așteptați … + Vă rugăm să așteptați ... + Vă rugăm să așteptați ... Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. @@ -458,7 +458,7 @@ Vezi citit Vezi necitit A apărut o eroare la alegerea imaginilor - Vă rugăm să așteptați … + Vă rugăm să așteptați ... Imaginile de Calitate sunt imagini ale unor fotografi și ilustratori de înaltă calificare, pe care comunitatea Wikimedia Commons a ales-o ca fiind de cea mai înaltă calitate pe site. Imaginile Încărcate prin Locurile din Apropiere sunt imaginile care sunt încărcate prin descoperirea locurilor de pe hartă. Această caracteristică permite editorilor să trimită o notificare de Mulțumire utilizatorilor care fac modificări utile - folosind un mic link de mulțumire pe pagina istoric sau pe pagina dif. @@ -478,7 +478,7 @@ Numere Serie Software Încărcați fotografii pe Wikimedia Commons direct de pe telefon. Descărcați aplicația Commons acum: %1$s - Partajează aplicația prin … + Partajează aplicația prin ... Informații despre imagine Nu s-au găsit categorii Nu s-au Găsit Reprezentări diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 861d7ee27c..b15d777876 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -45,7 +45,7 @@ * ЛингвоЧел * ОйЛ --> - + Facebook-страница Викисклада Исходный код Викисклада на гитхабе Логотип Викисклада @@ -105,7 +105,7 @@ %1$d загрузок - Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства + Получение общего содержимого. Обработка изображения может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства Получение общего содержимого. Обработка изображений может занять некоторое время, в зависимости от размера изображения и модели вашего устройства @@ -556,7 +556,7 @@ Отказано в доступе к местоположению файла Возможно, мы не сможем автоматически получать данные о местоположении из загруженных вами изображений. Пожалуйста, добавьте подходящее место для каждого изображения перед отправкой Загружайте фото на Викисклад прямо с телефона. Скачайте приложение Wikimedia Commons прямо сейчас: %1$s - Поделиться приложением с помощью… + Поделиться приложением с помощью... Информация об изображении Категории не найдены. Описания не найдены @@ -637,7 +637,7 @@ Добавлено в закладки Что-то пошло не так. Не удалось установить фоновую заставку Сделать фоновой заставкой - Идёт установка фоновой заставки… + Идёт установка фоновой заставки... Настройки системы Тёмная Светлая @@ -695,8 +695,8 @@ Режим ограниченного подключения Качественные изображения Качественные изображения - это диаграммы или фотографии, которые соответствуют определенным стандартам качества (которые в основном носят технический характер) и представляют ценность для проектов Викимедиа - Возобновление загрузки… - Приостановка загрузки… + Возобновление загрузки... + Приостановка загрузки... Отмена загрузки… Отменить загрузку Вы включили ограниченный режим подключения. Все загрузки приостановлены и возобновятся после отключения этого режима. @@ -841,7 +841,7 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок… + Отмена всех загрузок... Загрузки В ожидании Не удалось diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index d4b5916593..08f9a1fecc 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -169,7 +169,7 @@ ھا! وڌيڪ معلومات زمرا - لاهيندي… + لاهيندي... ڪوبہ چونڊيل ناھي عنوان ناهي ڪا تشريح ناھي @@ -315,7 +315,7 @@ لينس ماڊل سيريل انگ سافٽويئر - ايپ ذريعي ونڊيو… + ايپ ذريعي ونڊيو... عڪس معلومات زمرا نہ لڌا رد-ڪيل چاڙھ diff --git a/app/src/main/res/values-se/strings.xml b/app/src/main/res/values-se/strings.xml index 0489c363a1..78114e3362 100644 --- a/app/src/main/res/values-se/strings.xml +++ b/app/src/main/res/values-se/strings.xml @@ -44,9 +44,9 @@ Vajáldahttetgo beassansáni? Searvva Čáliha sisa - Vuordil… + Vuordil... Ođasmáhttá govvateavsttaid ja govvádusaid - Vuordil… + Vuordil... Sisačáliheapmi lihkostuvai! Sisačáliheapmi ii lihkostuvvan! Fiila ii gávdnon. Geahččal áinnas eará fiilla. @@ -112,7 +112,7 @@ Atte máhcahaga (e-poasttain) Ii leat ásahuvvon epoastadoaimmaheaddji Áitto geavahuvvon kategoriijat - Vuordime vuosttaš synkroniserema… + Vuordime vuosttaš synkroniserema... It leat vel bajásluđen ovttage gova. Geahččal ođđasit Gaskkalduhte @@ -143,7 +143,7 @@ Jua! Lassedieđut Kategoriijat - Luđeme… + Luđeme... Ii guhtege válljejuvvon Ii leat govvateaksta Ii gávdno govvádus diff --git a/app/src/main/res/values-sh/strings.xml b/app/src/main/res/values-sh/strings.xml index 8e9cde75c9..5997677efd 100644 --- a/app/src/main/res/values-sh/strings.xml +++ b/app/src/main/res/values-sh/strings.xml @@ -112,7 +112,7 @@ Pošaljite Vašu povratnu informaciju (putem e-pošte) Nemate uspostavljen klijent za e-poštu Nedavno korištene kategorije - Čekam prvo usklađivanje… + Čekam prvo usklađivanje... Još uvijek niste otpremili nijednu sliku. Pokušaj ponovo Otkaži @@ -147,7 +147,7 @@ Da! Više informacija Kategorije - Učitavanje… + Učitavanje... Ništa nije odabrano Nema opisa Nema razgovora diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 0e661acb74..92fa25f3eb 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -5,24 +5,25 @@ * Sandaru * හරිත --> - + කොමන්ස් ෆේස්බුක් පිටුව කොමන්ස් ලාන්චනය කොමන්ස් වෙබ් අඩවිය - 1 ගොනුවක් උඩුගත කෙරේ + 1 ගොනුවක් උඩුගත කෙරේ ගොනු %d ක් උඩුගත කෙරේ - එක් උඩුගත කිරීමක් ඇත + තවමත් කිසිදු උඩුගත කිරීමක් නැත + එක් උඩුගත කිරීමක් ඇත උඩුගත කිරීම් %1$d ක් ඇත - 1 උඩුගත කිරීමක් ආරම්භ කරමින් + 1 උඩුගත කිරීමක් ආරම්භ කරමින් උඩුගත කිරීම් %1$d ක් ආරම්භ කරමින් - 1 උඩුගත කිරීමක් + 1 උඩුගත කිරීමක් උඩුගත කිරීම් %1$d ක් මෙම පින්තූරය %1$s යටතේ වලංගු වනු ඇත diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 99a0bf5483..49fc88a3b0 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -491,7 +491,7 @@ Zobraziť prečítané Zobraziť neprečítané Nastala chyba pri vyberaní obrázkov - Čakajte, prosím… + Čakajte, prosím... Najlepšie obrázky sú fotografie od vysoko skúsených fotografov a ilustrátorov, ktoré vybrala komunita Wikimedie Commons ako jedny z najkvalitnejších na stránke. Obrázky nahrané cez Miesta v okolí sú obrázky, ktoré sú nahrané vďaka objavovaniu miest na mape. Táto funkcia umožňuje poslať poďakovanie za užitočné úpravy používateľom – použitím malého odkazu poďakovať v histórií stránky alebo na stránke rozdielu medzi revíziami. @@ -513,7 +513,7 @@ Prístup k polohe médií bol odmietnutý Možno nebudeme môcť automaticky získať údaje o polohe z obrázkov, ktoré nahráte. Pred odoslaním, prosím, pridajte ku každému obrázku údaj o polohe. Nahrávajte fotky na Wikimedia Commons priamo z vášho mobilu. Stiahnite si aplikáciu Wikimedia Commons teraz: %1$s - Zdieľať aplikáciu cez… + Zdieľať aplikáciu cez... Informácie o obrázku Nenájdené žiadne kategórie Neboli nájdené spôsoby vykreslovania @@ -593,7 +593,7 @@ Pridané do záložiek Niečo sa pokazilo. Tapetu sa nepodarilo nastaviť Nastaviť ako tapetu - Nastavujem tapetu. Prosím, čakajte… + Nastavujem tapetu. Prosím, čakajte... Predvolený systém Tmavý Svetlý @@ -651,9 +651,9 @@ Mód limitovaného pripojenia Kvalitné obrázky Kvalitné obrázky sú diagramy a fotografie, ktoré spĺňajú určité štandardy (ktoré sú väčšinou technického charakteru) a sú cenné pre projekty Wikimédie - Pokračovanie nahrávania… - Pozastavovanie nahrávania… - Prerušovanie nahrávania… + Pokračovanie nahrávania... + Pozastavovanie nahrávania... + Prerušovanie nahrávania... Zrušiť nahrávanie Zapli ste mód limitovaného pripojenia. Všetky nahrávania budú teraz pozastavené a budú pokračovať až po vypnutí tohto módu. Mód limitovaného pripojenia je zapnutý. @@ -787,7 +787,7 @@ Iný problém alebo informácia (vysvetlite nižšie). Vaša spätná väzba sa zverejní na nasledujúcej wiki stránke: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Ste si istí, že chcete zrušiť všetky nahrávania? - Ruším všetky nahrávania… + Ruším všetky nahrávania... Nahrané súbory Čakajúce Zlyhané diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index b91c3c0b13..61531980f1 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -6,7 +6,7 @@ * McDutchie * Upwinxp --> - + Facebook stran Zbirke Izvorna koda Zbirke v shrambi Github Logotip Zbirke @@ -66,8 +66,8 @@ %1$d nalaganj - Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. - Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slike lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. + Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. Prejemam deljeno vsebino. Obdelava slik lahko traja nekaj časa, odvisno od velikosti slike in vaše naprave. @@ -87,9 +87,9 @@ Ste pozabili geslo? Ustvari račun Prijavljanje - Prosimo, počakajte … + Prosimo, počakajte ... Posodabljam napise in opise - Prosimo, počakajte … + Prosimo, počakajte ... Uspešno ste se prijavili! Prijava ni uspela! Datoteka ni bila najdena. Prosimo, poskusite z drugo datoteko. @@ -133,7 +133,7 @@ Spremembe Naloži Poišči kategorije - Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, …) + Poiščite predmete, ki jih vaša predstavnostna datoteka prikazuje (gora, Tadž Mahal, ...) Shrani Osveži Seznam @@ -159,7 +159,7 @@ Pošljite povratno informacijo (prek e-pošte) Nameščen ni noben e-poštni odjemalec Pred kratkim uporabljene kategorije - Čakam na prvo sinhronizacijo … + Čakam na prvo sinhronizacijo ... Naložili niste še nobene fotografije. Poskusi znova Prekliči @@ -199,7 +199,7 @@ Da! Več informacij Kategorije - Nalaganje … + Nalaganje ... Nič ni izbrano Ni napisa Ni opisa @@ -491,7 +491,7 @@ Ogled prebranih Ogled neprebranih Pri izbiri slik je prišlo do napake - Prosimo, počakajte … + Prosimo, počakajte ... Izbrane slike so slike izvrstnih fotografov in ilustratorjev, ki jih je skupnost Wikimedijine zbirke prepoznala kot najbolj kakovostne v tem projektu. Slike, naložene z Bližnjimi kraji, so slike, ki so naložene z odkrivanjem krajev na zemljevidu. Ta možnost vam omogoča, da urejevalcem, ki so opravili koristno urejanje, pošljete zahvalo – z uporabo kratke povezave na strani zgodovine ali strani primerjave. @@ -513,7 +513,7 @@ Dostop do lokacije predstavnosti zavrnjen Za slike, ki jih nalagate, ne moremo samodejno pridobiti lokacije. Pred pošiljanjem dodajte za vsako sliko ustrezno lokacijo. Nalagajte slike v Wikimedijino zbirko neposredno iz telefona. Prenesite aplikacijo Commons zdaj: %1$s - Deli aplikacijo prek … + Deli aplikacijo prek ... Informacije o sliki Ni najdenih kategorij Ni najdenih upodobitev @@ -569,7 +569,7 @@ Koordinat ni bilo mogoče pridobiti. Ni bilo mogoče pridobiti opisov. Uredi opise in napise - Deli slike prek … + Deli slike prek ... Ničesar še niste prispevali %s ni opravil_a še nobenega prispevka Račun ustvarjen! @@ -593,7 +593,7 @@ Dodano med zaznamke Nekaj je šlo narobe. Ozadja ni bilo mogoče nastaviti. Nastavi kot ozadje - Nastavljam ozadje. Prosimo, počakajte … + Nastavljam ozadje. Prosimo, počakajte ... Sledi sistemu Temna Svetla @@ -649,9 +649,9 @@ Način omejene povezanosti Kakovostne slike Kakovostne slike so ponazoritve ali fotografije, ki ustrezajo nekaterim merilom kakovosti (ta so predvsem tehnična) in so dragocene za projekte Wikimedie - Nalaganje se nadaljuje… - Zaustavljam nalaganje… - Preklicujem nalaganje… + Nalaganje se nadaljuje ... + Zaustavljam nalaganje ... + Preklicujem nalaganje ... Preklic nalaganja Vklopili ste način omejene povezanosti. Vsa nalaganja so začasno ustavljena in se bodo nadaljevala, ko boste ta način izklopili. Način omejene povezanosti je vklopljen. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index f1e7412d48..40e99618fe 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -34,39 +34,32 @@ Слика дана %1$d датотека се отпрема - %1$d датотеке се отпремају %1$d датотеке се отпремају %1$d отпремање - %1$d отпремања %1$d отпремања Покретање отпремања Процесуирање %d отпремање - Процесуирање %d отпремања Процесуирање %d отпремања %d отпремање - %1$d отпремања %d отпремања Слика ће се водити под лиценцом %1$s - Слике ће се водити под лиценцом %1$s Слике ће се водити под лиценцом %1$s %1$d отпремање - %1$d отпремања %1$d отпремања - Пријем %d дељеног садржаја… Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја - Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја - Пријем %d дељеног садржаја… Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја + Примање дељеног садржаја... Процесуирање слике може потрајати неко време, у зависности од величине слике и вашег уређаја + Примање дељеног садржаја... Процесуирање слика може потрајати неко време, у зависности од величине слика и вашег уређаја Истрага Изглед @@ -503,7 +496,7 @@ Приступ локацији медија је одбијен Можда нећемо моћи да аутоматски прибавимо податке о локацији из слика које отпремите. Додајте одговарајућу локацију за сваку слику пре објављивања Отпреми фотографије на Викимедијину Оставу директно са свог телефона. Преузми апликацију Оставе сада: %1$s - Подели апликацију преко… + Подели апликацију преко... Информације о слици Нису пронађене категорије Отказано отпремање @@ -528,13 +521,12 @@ Успешно Категорија %1$s је додата. - Категорије %1$s су додате. Категорије %1$s су додате. Није могуће додати категорије. Ажурирај категорију Уреди приказе - Ажурирање координата… + Ажурирање координата... Ажурирање координата Ажурирање описа Ажурирање натписа @@ -737,7 +729,6 @@ Чување GPX датотеке %d слика је одабрана - %d слике су одабране %d слика је одабрано Унесите коментар @@ -746,4 +737,6 @@ Отпремања На чекању Није успело + Ово место већ има слику + Проверавање да ли ово место има слику. diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index 64379ac928..79ae5ea28f 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -26,25 +26,32 @@ Togel ka Luhur Gambar poé ieu + ngunjal %1$d berkas ngunjal %1$d berkas + (%1$d) (%1$d) Mitembeyan Ngamuat + Ngolah %d muatan Ngolah %d muatan + %1$d muatan %1$d muatan + Ieu gambar bakal dilisénsi %1$s Ieu gambar bakal dilisénsi %1$s + %1$d Dimuat %1$d Dimuat + Nampa kontén anu dibagikeun. Ngolah gambarna bisa jadi rada lila gumantung kana ukuran gambar jeung gaway anjeun Nampa kontén anu dibagikeun Langlang @@ -64,7 +71,7 @@ Asup log Tungguan… Nganyarkeun pertélaan jeung pedaran - Mangga tungguan… + Mangga tungguan... Laksana login! Gagal login! Berkas teu kapanggih. Coba berkas séjén. @@ -392,7 +399,7 @@ Tempo arsip Tempo nu can dibaca Éror pas keur nyomot gambar - Mangga tungguan… + Mangga tungguan... Iwalkeun ieu gambar Karya Hak cipta @@ -402,7 +409,7 @@ Nomer Seri Sopwér Muat poto ka Wikimedia Commons langsung tina ponsél. Unduh Commons App ayeuna: %1$s - Bagikeun app liwat… + Bagikeun app liwat... Info Gambar Euweuh Kategori kapanggih Muatan bedo diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index c49d64ad22..370bf0915f 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -506,7 +506,7 @@ Åtkomst till mediaplats nekades Vi kanske inte automatiskt kan få platsdata från bilder du laddar upp. Lägg till lämplig plats för varje bild innan du skickar in Ladda upp foton till Wikimedia Commons direkt från din telefon. Ladda ned Commons-appen nu: %1$s - Dela appen via… + Dela appen via... Bildinfo Inga kategorier hittades Inga beskrivningar hittades @@ -783,7 +783,7 @@ Andra problem eller information (ange nedan). Din återkoppling kommer att skickas till följande wikisida: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobilapp/Återkoppling</a> Är du säker på att du vill avbryta alla uppladdningar? - Avbryter alla uppladdningar… + Avbryter alla uppladdningar... Uppladdningar Pågår Misslyckades diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4f41da5f6d..f9162bc7b4 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -98,7 +98,7 @@ பின்னூட்டம் அனுப்பு (மின்னஞ்சல் வழியாக) மின்னஞ்சற் செயலி எதுவும் நிறுவப்படவில்லை அண்மையிற் பயன்படுத்தப்பட்ட பகுப்புகள் - முதல் ஒத்திசைவுக்காக காத்திருக்கிறது … + முதல் ஒத்திசைவுக்காக காத்திருக்கிறது ... நீர் இன்னும் எவ்வொளிப்படத்தையும் பதிவேற்றவில்லை. மீண்டும் முயல்க கைவிடு @@ -131,7 +131,7 @@ ஆம்! மேலதிக தகவல்கள் பகுப்புகள் - ஏற்றப்படுகிறது… + ஏற்றப்படுகிறது... தெரிவு செய்யப்படவில்லை தலைப்பு இல்லை விளக்கம் இல்லை diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index 13ee985b95..add46f7b7b 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -110,7 +110,7 @@ ಇರೆನ ಅಬಿಪ್ರಾಯೊ ಬರೆಲೆ(ಮಿಂಚಂಚೆ). ಇರೆನ ಮಿಂಚಂಚೆ ಇಜ್ಜಿ. ಇಂಚಿಗ್ ಸೃಷ್ಟಿ ಮಾಲ್ತಿನ ವರ್ಗೊ. - ಒಂತೆ ಸಮಯ ಕಾಯೊಡು…. + ಒಂತೆ ಸಮಯ ಕಾಯೊಡು.... ಇರ್ ಒಂಜಿಲಾ ಪಟೋನ್ ಅಪ್ಲೋಡ್ ಮಾಲ್ತಿಜ್ಜಿ. ನನೊರ ಪ್ರಯತ್ನ ಮಾನ್ಪುಲೇ ವಜಾ ಮಲ್ಪುಲೆ @@ -336,7 +336,7 @@ ಅನುರಕ್ಷಿತ ತೂಲೆ ಓದಂದಿನ ತೂಲೆ ಆಕೃತಿಲೆನ್ ಪೆಜ್ಜಿನಗ ದೋಷ ಆಂಡ್ - ದಯಮಲ್ತ್ ಕಾಪುಲೆ… + ದಯಮಲ್ತ್ ಕಾಪುಲೆ... ಸಂಯೋಜನೆಲು ಸೂಚನೆಲು ನನಾತ್ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index 0c478b2214..ae80a53357 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -127,7 +127,7 @@ ఫీడుబ్యాకును పంపండి (ఈమెయిలు ద్వారా) ఈమెయిలు క్లయంటేదీ లేదు ఇటీవల వాడిన వర్గాలు - మొట్టమొదటి సింక్ కోసం చూస్తున్నాం… + మొట్టమొదటి సింక్ కోసం చూస్తున్నాం... ఇంకా మీరు ఫోటోలేమీ ఎక్కించలేదు. మళ్ళీ ప్రయత్నించు రద్దుచేయి @@ -457,7 +457,7 @@ క్రమ సంఖ్యలు సాఫ్టువేరు నేరుగా మీ ఫోను నుంచే వికీమీడియా కామన్స్‌కు ఫోటోలను ఎక్కించండి. కామన్స్ యాప్‌ను ఇప్పుడే దించుకోండి: %1$s - యాప్‌ను దీని ద్వారా పంచుకోండి… + యాప్‌ను దీని ద్వారా పంచుకోండి... బొమ్మ సమాచారం వర్గాలేమీ కనబడలేదు ఎక్కింపును రద్దు చేసాం @@ -523,7 +523,7 @@ బుక్‌మార్కులకు చేర్చాం ఏదో లోపం జరిగింది. వాల్‌పేపరును సెట్ చెయ్యలేకపోయాం వాల్‌పేపరుగా అమర్చు - వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి… + వాల్‌పేపరుగా సెట్ చేస్తున్నాం. కాస్త ఆగండి... నల్లటి వెలుగుతో స్థానపు సెట్టింగులను తెరవడం విఫలమైంది. స్థానాన్ని మానవికంగా ఆన్ చెయ్యండి @@ -576,9 +576,9 @@ పరిమిత కనెక్షను మోడ్‌ను అచేతనం చేసాం. పెండింగులో ఉన్న ఎక్కింపులు తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ నాణ్యమైన బొమ్మలు - ఎక్కింపును తిరిగి మొదలెడుతున్నాం… - ఎక్కింపును నిలుపుతున్నాం… - ఎక్కింపును రద్దు చేస్తున్నాం… + ఎక్కింపును తిరిగి మొదలెడుతున్నాం... + ఎక్కింపును నిలుపుతున్నాం... + ఎక్కింపును రద్దు చేస్తున్నాం... ఎక్కింపును రద్దుచెయ్యి మీరు పరిమిత కనెక్షను మోడ్‌ను చేతనం చేసారు. ఎక్కింపులన్నీ నిలిచిపోయాయి. మీరు ఈ మోడ్‌ను అచేతనం చెయ్యగానే అవి తిరిగి మొదలౌతాయి. పరిమిత కనెక్షను మోడ్ ఆన్ అయింది. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 70bee59ef5..125bba590f 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -38,16 +38,21 @@ รูปภาพประจำวัน กำลังอัปโหลดไฟล์ %1$d ไฟล์ + \@string/contributions_subtitle_zero + (%1$d) (%1$d) กำลังเริ่มอัปโหลด + กำลังเริ่มอัปโหลด %1$d รายการ กำลังเริ่มอัปโหลด %1$d รายการ + การอัปโหลด %1$d รายการ การอัปโหลด %1$d รายการ + ภาพนี้จะอยู่ในสัญญาอนุญาต %1$s ภาะเหล่านี้จะอยู่อยู่ในสัญญาอนุญาติ %1$s สำรวจ @@ -393,7 +398,7 @@ รุ่นเลนส์ หมายเลขซีเรียล ซอฟต์แวร์ - แบ่งปันแอปผ่าน… + แบ่งปันแอปผ่าน... ไม่พบหมวดหมู่ ภาพเซลฟี ภาพเบลอ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 79482fb4e9..31a9f0b530 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -210,7 +210,7 @@ Evet! Daha Fazla Bilgi Kategoriler - Yükleniyor… + Yükleniyor... Hiçbir şey seçilmedi Altyazı yok Açıklama yok @@ -505,7 +505,7 @@ Okunanları görüntüle Okunmayanları görüntüle Resimler seçilirken hata oluştu - Lütfen bekleyin… + Lütfen bekleyin... Seçkin resimler, Wikimedia Commons topluluğunun sitedeki en yüksek kaliteden bazıları olarak seçtiği son derece yetenekli fotoğrafçıların ve illüstratörlerin görüntüleridir. Yakındaki yerler üzerinden yüklenen resimler, haritadaki yerleri keşfederek yüklenen resimlerdir. Bu özellik, editörlerin, geçmiş sayfasında veya fark sayfasında küçük bir teşekkür bağlantısı kullanarak faydalı düzenlemeler yapan kullanıcılara bir Teşekkür bildirimi göndermesine olanak tanır. @@ -604,7 +604,7 @@ Yer işaretlerine eklendi Bir şeyler yanlış gitti. Duvar kağıdı ayarlanamadı Duvar kağıdı olarak ayarla - Duvar Kağıdı ayarlanıyor. Lütfen bekleyin… + Duvar Kağıdı ayarlanıyor. Lütfen bekleyin... Sistemi izle Koyu Açık diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cc2343a771..9e821ae24d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,7 @@ * Ата * Пан Хаунд --> - + Facebook-сторінка Вікісховища Програмний код Вікісховища на GitHub Логотип Вікісховища @@ -81,7 +81,7 @@ %1$d завантажень - Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою + Отримання спільного контенту. Обробка зображення може зайняти трохи часу, залежно від розміру зображення і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою Отримання спільного контенту. Обробка зображень може зайняти трохи часу, залежно від розміру зображень і від Вашого пристрою @@ -612,7 +612,7 @@ Додано у закладки Щось трапилось. Не вдалося встановити шпалери робочого столу Встановити в якості шпалер робочого столу - Встановлення робочого столу. Будь ласка зачекайте… + Встановлення робочого столу. Будь ласка зачекайте... На взірець системи Темна Світла diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index f09f76ac62..a708c873a2 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -73,9 +73,9 @@ Parolni unutdingizmi? Roʻyxatdan oʻtish Kirish - Iltimos kuting… + Iltimos kuting... Sarlavhalar va tavsiflarni yangilash - Iltimos, kutib turing… + Iltimos, kutib turing... Kirish muvaffaqiyatli bajarildi! Kirish muvaffaqiyatsiz yakunlandi! Fayl topilmadi. Iltimos, boshqa faylni izlab koʻring. @@ -180,7 +180,7 @@ Ha! Batafsil maʼlumot Turkumlar - Yuklanmoqda… + Yuklanmoqda... Tanlanmagan Izoh yoʻq Tavsif yoʻq @@ -390,7 +390,7 @@ Xatchoʻplar Xatchoʻplar Bajarildi - Iltimos, kuting… + Iltimos, kuting... EXIF teglarni boshqarish Muallif Mualliflik huquqlari diff --git a/app/src/main/res/values-vec/strings.xml b/app/src/main/res/values-vec/strings.xml index 52bb495ea8..bbcb64561a 100644 --- a/app/src/main/res/values-vec/strings.xml +++ b/app/src/main/res/values-vec/strings.xml @@ -68,7 +68,7 @@ Cargamento de %1$s no riusio Schicia par vixuałixare I me ultimi cargamenti - In coa… + In coa... Fałimento %1$d%% conpleto Drio cargar.. @@ -114,7 +114,7 @@ Mandane on comento (co ła mail) Nisun client de posta eletronega instałà Categorie doparà ultimamente - Speta par ła prima sincronixasion… + Speta par ła prima sincronixasion... No te ghe njiancora cargà na foto Riproa Descançełare @@ -403,7 +403,7 @@ Varda no lexeste Varda no lexeste Se ga vuo on eror co se jera drio ełexare łe imajini. - Speta on fià… + Speta on fià... Le foto in primo pian łe xé imajini de fotografi altamente cuałifegai che ła comunità de Wikimedia Commons ła ga ełeto come fotografi de alta cuałità sol sito. Imajini cargae via \"Posti cuà rente\", imajini che łe njien cargae scoerxendo posti n\'te ła mapa Sta funsion ła consente ai editori de enviar na notifega de ringrasiamento ai uxuari che i fa modifeghe che serve, doparando on lingambo picenin de ringrasiamento n\'te ła pajina del storego o n\'te ła pajina de łe difarense.\n\nQuesta funzione consente agli editor di inviare una notifica di ringraziamento agli utenti che apportano modifiche utili, utilizzando un piccolo link di ringraziamento nella pagina della cronologia o nella pagina delle differenze. @@ -421,7 +421,7 @@ Numari seriałi Software Carga foto so Wikimedia Commons diretamente dal to tełefonin. Descarga l\'aplicasion deso: %1$s - Spartisi aplicasion co… + Spartisi aplicasion co... Informasion so l\'imajine Nisuna categoria catada Cargamento nułà @@ -461,7 +461,7 @@ Xonta ai favorii Calcosa el xé ndà roerso. No xé sta pusibiłe canbiar el sfondo Inposta el sfondo - Drio inpostar el sfondo. Speta on fià… + Drio inpostar el sfondo. Speta on fià... Segui el sistema Scuro Ciaro diff --git a/app/src/main/res/values-xal/strings.xml b/app/src/main/res/values-xal/strings.xml index c362060613..346ff15e17 100644 --- a/app/src/main/res/values-xal/strings.xml +++ b/app/src/main/res/values-xal/strings.xml @@ -23,15 +23,15 @@ Вики-аһулх һазр Тохрллһ Вики-аһулх һазрур ацалх - Ацалгдҗана… + Ацалгдҗана... Кергләчин нерн Нууц үг Невтрх Нууц үгән мартвт? Бүрткүлх Невтрҗәнә - Күләхнтн… - Күләхнтн… + Күләхнтн... + Күләхнтн... Невтрлт амҗлтта болла! Невтрҗ чадсн уга! Ацаллт кеҗ экллә! @@ -83,7 +83,7 @@ Тиим Делгрңгү Нерн, төрл - Умшҗана… + Умшҗана... Алькинь чигн суңһад уга Тодрхаллт уга Күүндән уга diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index 7927da1af5..b32eeb0057 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -61,7 +61,7 @@ ვიკიოწკარუე პარამეტრეფი ვიკიოწკარუეშა ეხარგუა - ეთმიხარგუ… + ეთმიხარგუ... მახვარებუშ ჯოხო პაროლი გენშართით თქვანი პროფილით Commons Beta-შა @@ -71,7 +71,7 @@ სისტემაშა მიშულა ქორთხინთ ქჷმიცადით … მუკნაჭარეფი დო ეჭარუეფი მითმიახალებუ - ქორთხინთ ქჷმიცადით… + ქორთხინთ ქჷმიცადით... სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. @@ -181,7 +181,7 @@ ქოǃ უმოსი ინფორმაცია კატეგორიეფი - იხარგუ… + იხარგუ... მუთუნ ვა რე გიშაგორილი მუკნაჭარა ვა რე ვა რე ეჭარუა diff --git a/app/src/main/res/values-zgh/strings.xml b/app/src/main/res/values-zgh/strings.xml index c6f27bb991..27080b9991 100644 --- a/app/src/main/res/values-zgh/strings.xml +++ b/app/src/main/res/values-zgh/strings.xml @@ -13,7 +13,7 @@ ⵜⴻⵜⵜⵓⴷ ⵜⴰⴳⵓⵔⵉ ⵏ ⵓⵣⵣⵔⴰⵢ? ⵣⵎⵎⴻⵎ ⴷⴰ ⵜⴽⵛⵛⵎⴷ - ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ… + ⴰⵎⵓⵔ ⵏⵏⴽ ⵇⵇⵍ... ⴰⴽⵛⴰⵎ !ⵉⵎⵓⵔⵙ ⴰⴽⵛⴰⵎ ⵉⵣⴳⵍ! ⴰⴼⴰⵢⵍⵓ ⵓⵔ ⵉⵜⵜⵢⵓⴼⴰ. ⴰⵎⵓⵔ ⵏⵏⴽ ⴰⵔⵎ ⴰⴼⴰⵢⵍⵓ ⵢⴰⴹⵏ. diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2a307e955c..2adabec26e 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -79,22 +79,28 @@ 地点状态 今日图片 + %1$d个文件正在上传 %1$d个文件正在上传 + %1$d次上传 %1$d次上传 开始上传 + 正在处理%d个上传 正在处理%d个上传 + %d个上传 %d个上传 + 该图像的授权协议是 %1$s 这些图像的授权协议是 %1$s + %1$d次上传 %1$d次上传 @@ -546,7 +552,7 @@ 已拒绝访问媒体位置 我们可能无法自动从你上传的图片中获取位置数据。提交前请为每张图片添加适当的位置 直接在您手机上的维基共享资源应用中上传照片。立即下载共享资源应用:%1$s - 分享到… + 分享到... 图像信息 找不到分类 找不到描写。 @@ -572,6 +578,7 @@ 分类更新 成功 + 分类%1$s已添加。 分类%1$s已添加。 无法添加分类。 @@ -579,6 +586,7 @@ 正在尝试更新描述。 编辑描述 + 已添加 %1$s 个描写。 已添加 %1$s 个描写。 无法添加描述。 @@ -679,8 +687,8 @@ 限制连接模式 优良图片 品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值 - 正在恢复上传… - 暂停上传… + 正在恢复上传... + 暂停上传... 正在取消上传… 取消上传 您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。 @@ -808,6 +816,7 @@ 正在保存KML文件 正在保存GPX文件 + 已选择%d个图像 已选择%d个图像 请记住,每次多图片上传会为其中的所有图片标注相同的分类和描述。如果这些图片并不共享同样的描述和分类,请分别进行多次上传。 @@ -821,9 +830,12 @@ 其他问题或信息(请在下方解释)。 您的反馈已经发布在以下wiki页面:<a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> 您确定要取消所有上传吗? - 取消所有的上传… + 取消所有的上传... 上传 待处理 失败 无法加载地点数据 + 这个地点还没有照片,快去拍一张吧! + 这个地点已经有照片了。 + 现在检查这个地点是否有照片。 From 7c826502b676400a513bed7f64206701f45425d9 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 31 Oct 2024 13:01:42 +0100 Subject: [PATCH 008/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-krc/strings.xml | 12 +++++++++++- app/src/main/res/values-pa/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 2 ++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index 55fa4ac359..eb9193e7f0 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -473,6 +473,7 @@ Окъулмагъана хапарландырыуугъуз джокъду Окъулмагъан хапарландырыуугъуз барды Логларыгъызны хайырланыб юлюшлегиз + Электрон почтагъызны тинтигиз Окъулгъанны кёргюз Окъулмагъанланы кёргюз Суратла сайланнган заманда халат болду. @@ -686,7 +687,7 @@ Джууукъдагъы картала тюз ишлер ючюн ТЕЛЕФОННУ БОЛУМУн окъургъа амал болургъа кереклиди Хайырланыучуну къошумлары: %s Хайырланыучуну джетишимлери: %s - Хайырланыучу бетни кёргюз + Хайырланыучу профильни кёр Танытыуланы тюзет Категорияланы тюзет Кенгленнген Сайлаула @@ -774,4 +775,13 @@ \'%1$s\' - башха джерди. Тилейбиз, тюз джерни энишгерекде белгилегиз, эмда мадар бар эсе, тюз кенглик бла узунлукъну джазыгъыз. Башха проблема неда информация (тилейбиз, энишгерекде ангылатыгъыз). Сизни кери оюмугъуз тюбюндеги вики бетге джиберилликди: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + Бютеу джюклеулени тохтатыргъа излегенигизге ишексизмисиз? + Бютеу джюклеулени тохтатыу... + Джюклеуле + Сакълауда + Джетишимсиз + Джерни юсюнден билгилени джюклеялмады + Бу джерни сураты джокъду, хайда бирин эт! + Бу джерни алайсыз да сураты барды. + Бу джерни сураты болуб-болмагъанын тинте турама. diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8c64900a56..ebc535d50d 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -207,5 +207,6 @@ ਲਿਖਤ ਛਾਪੋ ਮੁਹਰੈਲ ਵਰਤੋਂਕਾਰ + ਟਿਕਾਣਾ ਨਵਿਆਈਆ ਗਿਆ ਤੁਹਾਡੇ ਦਾਖਲੇ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਦਾਖਲ ਹੋਵੋ। diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 224492ab72..635d71a3fa 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,6 +1,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_custom_selector.xml b/app/src/main/res/menu/menu_custom_selector.xml new file mode 100644 index 0000000000..fc432439e4 --- /dev/null +++ b/app/src/main/res/menu/menu_custom_selector.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f9de8e0513..187c5fc96f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -117,6 +117,7 @@ Search categories Search for items that your media depicts (mountain, Taj Mahal, etc.) Save + Overflow menu Refresh List (No uploads yet) @@ -832,6 +833,17 @@ Upload your first media by tapping on the add button. Pending Failed Could not load place data + + Delete Folder + Confirm Deletion + Are you sure you want to delete folder %1$s containing %2$d items? + Delete + Cancel + Folder %1$s deleted successfully + Failed to delete folder %1$s + Error in trashing folder contents: %1$s + Failed to retrieve folder path for bucket ID: %1$d + This place has no picture yet, go take one! This place has a picture already. Now checking whether this place has a picture. diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 8ac890545d..5d2dabb599 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -51,6 +51,13 @@ android:summary="@string/display_campaigns_explanation" android:title="@string/display_campaigns" /> + + Date: Thu, 14 Nov 2024 13:35:05 +1100 Subject: [PATCH 020/231] Improve Unique File Name Search (#5877) * Modified findUniqueFileName() in UploadWorker.kt to use a random 3-digit alphanumeric hash to append to a file name to make it unique. This improves speed over using an incrementing number to append as there are fewer collisions. * Modified findUniqueFileName() in UploadWorker.kt to use a random 5-digit numeric hash rather than the previous 3-digit alphanumeric hash * Removed unnecessary variable "chars" --------- Co-authored-by: Jinniu Du <127721018+Donutcheese@users.noreply.github.com> Co-authored-by: Zihan Pan Co-authored-by: Nicolas Raoul --- .../nrw/commons/upload/worker/UploadWorker.kt | 46 +++++++++---------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 144c503bba..00cd29a6d9 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -47,6 +47,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Date +import java.util.Random import java.util.regex.Pattern import javax.inject.Inject @@ -548,33 +549,30 @@ class UploadWorker( } private fun findUniqueFileName(fileName: String): String { - var sequenceFileName: String? - var sequenceNumber = 1 - while (true) { + var sequenceFileName: String? = fileName + val random = Random() + + // Loops until sequenceFileName does not match any existing file names + while (mediaClient + .checkPageExistsUsingTitle( + String.format( + "File:%s", + sequenceFileName, + ), + ).blockingGet()) { + + // Generate a random 5-character alphanumeric string + val randomHash = (random.nextInt(90000) + 10000).toString() + sequenceFileName = - if (sequenceNumber == 1) { - fileName + if (fileName.indexOf('.') == -1) { + "$fileName #$randomHash" } else { - if (fileName.indexOf('.') == -1) { - "$fileName $sequenceNumber" - } else { - val regex = - Pattern.compile("^(.*)(\\..+?)$") - val regexMatcher = regex.matcher(fileName) - regexMatcher.replaceAll("$1 $sequenceNumber$2") - } + val regex = + Pattern.compile("^(.*)(\\..+?)$") + val regexMatcher = regex.matcher(fileName) + regexMatcher.replaceAll("$1 #$randomHash") } - if (!mediaClient - .checkPageExistsUsingTitle( - String.format( - "File:%s", - sequenceFileName, - ), - ).blockingGet() - ) { - break - } - sequenceNumber++ } return sequenceFileName!! } From 248c7b0ceb0074d1761d099fb0bd99fd297da177 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 14 Nov 2024 13:02:04 +0100 Subject: [PATCH 021/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-ar/strings.xml | 10 ++++ app/src/main/res/values-cs/strings.xml | 62 +++++++++++++++++++------ app/src/main/res/values-da/strings.xml | 10 ++++ app/src/main/res/values-fr/strings.xml | 10 ++++ app/src/main/res/values-hi/strings.xml | 3 ++ app/src/main/res/values-it/strings.xml | 5 ++ app/src/main/res/values-ko/strings.xml | 9 ++++ app/src/main/res/values-lb/strings.xml | 3 ++ app/src/main/res/values-pa/strings.xml | 1 + app/src/main/res/values-pms/strings.xml | 9 ++++ app/src/main/res/values-sv/strings.xml | 4 ++ 11 files changed, 112 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e1f29eb2d9..acb7cd1226 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -171,6 +171,7 @@ تصنيفات البحث (.البحث عن العناصر التي تصورها وسائطك (جبل ،تاج محل، إلخ حفظ + القائمة الزائدة أنعش القائمة (لا يوجد تحميلات حتى الآن) @@ -850,6 +851,15 @@ قيد الانتظار فشل تعذر تحميل بيانات المكان + حذف المجلد + تأكيد الحذف + هل أنت متأكد أنك تريد حذف المجلد %1$s الذي يحتوي على %2$d عنصرا؟ + حذف + إلغاء + تم حذف المجلد %1$s بنجاح + فشل حذف المجلد %1$s + خطأ في نقل محتويات المجلد إلى سلة المهملات: %1$s + فشل استرداد مسار المجلد لمعرف الدلو: %1$d هذا المكان ليس له صورة بعد، اذهب والتقط واحدة! هذا المكان لديه صورة بالفعل. الآن التحقق ما إذا كان هذا المكان لديه صورة. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4d49ee6327..5ec23c385d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -5,6 +5,7 @@ * Chmee2 * Clon * Dvorapa +* Fjuro * Frettie * Georg101 * Ilimanaq29 @@ -28,31 +29,58 @@ Zdrojový kód Commons na GitHubu Logo Wikimedia Commons Stránka Commons + Ukončit výběr polohy Odeslat + Přidat další popis + Přidat nový příspěvek + Přidat příspěvek z fotoaparátu + Přidat příspěvek z fotek + Přidat příspěvek z galerie předchozích příspěvků + Titulky + Popis jazyka + Titulek + Popis + Obrázek + Vše + Přepnout nahoru + Zobrazení vyhledávání + Stát místa Obrázek dne - - %1$d soubor se nahrává - %1$d souborů se nahrává + + Nahrávání %1$d souboru + Nahrávání %1$d souborů + Nahrávání %1$d souborů + Nahrávání %1$d souborů - - \@string/contributions_subtitle_zero + (%1$d) + (%1$d) + (%1$d) (%1$d) - - Spouští se nahrávání %1$d souboru - Spouští se nahrávání %1$d souborů + Spouštění nahrávání + + Zpracovávání %d nahrání + Zpracovávání %d nahrání + Zpracovávání %d nahrání + Zpracovávání %d nahrání - - %1$d nahrávání - %1$d nahrávání + + %d nahrávání + %d nahrávání + %d nahrávání + %d nahrávání - + Tento obrázek bude zveřejněn pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s + Tyto obrázky budou zveřejněny pod licencí %1$s Tyto obrázky budou zveřejněny pod licencí %1$s - + %1$d nahrání + %1$d nahrání + %1$d nahrání %1$d nahrání @@ -67,6 +95,7 @@ Commons Nastavení Nahrát na Commons + Probíhá nahrávání Uživatelské jméno Heslo Přihlásit se do svého Commons beta účtu @@ -75,7 +104,9 @@ Zaregistrovat se Přihlášení Čekejte prosím… - Přihlášení uspělo! + Nahrávání titulků a popisů + Čekejte prosím… + Úspěšně přihlášeni! Přihlášení se nezdařilo! Soubor nebyl nalezen. Prosím, zkuste jiný soubor. Ověření se nezdařilo, prosím přihlaste se znovu @@ -488,7 +519,10 @@ Při přihlášení nastala chyba, musíte si resetovat vaše heslo! Místo v okolí nalezeno Je toto fotka místa %1$s? + Záložky Nastavení + Odebráno ze záložek + Přidáno do záložek Něco se pokazilo. Tapetu se nepodařilo nastavit Nastavit jako tapetu Nastavování tapety. Prosím, čekejte… diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 3b6822c47b..2b5dcc9144 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -125,6 +125,7 @@ Søg kategorier Søg efter genstande, som dine medier afbilder (bjerg, Taj Mahal osv.) Gem + Overløbsmenu Opdater Liste (Ingen uploads endnu) @@ -789,6 +790,15 @@ Afventer Mislykkedes Kunne ikke indlæse steddata + Slet mappe + Bekræft sletning + Er du sikker på, at du vil slette mappen %1$s, der indeholder %2$d elementer? + Slet + Annuller + Mappen %1$s blev slettet + Kunne ikke slette mappen %1$s + Fejl ved sletning af mappeindhold: %1$s + Kunne ikke hente mappestien til bucket-id: %1$d Dette sted har endnu ikke noget billede, så gå hen og tag et! Dette sted har allerede et billede. Tjekker nu, om dette sted har et billede. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ae4dfb9664..ecb27df91d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -161,6 +161,7 @@ Rechercher des catégories Rechercher les éléments que votre média représente (montagne, Taj Mahal, etc.). Enregistrer + Menu de débordement Actualiser Lister (Pas encore de téléversement) @@ -827,6 +828,15 @@ En attente Échec Les données du lieu n\'ont pas pu être chargées + Supprimer le dossier + Confirmer la suppression + Êtes-vous sûr de vouloir supprimer le dossier %1$s contenant %2$d éléments ? + Supprimer + Annuler + Le dossier %1$s a été supprimé avec succès + Impossible de supprimer le dossier %1$s + Erreur lors de la suppression du contenu du dossier : %1$s + Échec de la récupération du chemin d\'accès au dossier pour le bucket ID : %1$d Cet endroit n\'a pas encore de photo, allez en prendre une ! Cet endroit a déjà une photo. Je vérifie maintenant si cet endroit a une photo. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 50a04319b9..55a378eaf2 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -354,4 +354,7 @@ अपलोड लंबित विफल हुआ + फ़ोल्डर हटाएँ + हटाएँ + रद्द करें diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f408638708..538f6c884e 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -760,6 +760,11 @@ %d immagine selezionata %d immagini selezionate + Cancella cartella + Conferma cancellazione + Cancella + Annulla + Cartella %1$s cancellata correttamente Questo posto non ha ancora una foto, scattane una! Questo posto ha già una foto. Ora controlliamo se questo posto ha una foto. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index aa7ae98e79..1eaa3594ca 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -129,6 +129,7 @@ 분류 검색 미디어가 서술한 항목을 검색하세요. (산, 타지마할 등) 저장 + 오버플로 메뉴 새로 고침 목록 (아직 올린 항목이 없음) @@ -686,6 +687,14 @@ 보류 중 실패 장소 데이터를 불러오지 못했습니다 + 폴더 삭제 + 삭제 확인 + 항목 %2$d개를 포함하는 %1$s 폴더를 삭제하시겠습니까? + 삭제 + 취소 + %1$s 폴더를 성공적으로 삭제했습니다 + %1$s 폴더를 삭제하지 못했습니다 + 버킷 ID의 폴더 경로를 검색하지 못했습니다: %1$d 이 장소에 아직 사진이 없습니다. 사진을 찍어보세요! 이 장소에 이미 사진이 있습니다. 지금 이 장소에 사진이 있는지 확인 중입니다. diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index aa6f8e58db..f80784c564 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -522,4 +522,7 @@ %d Biller ausgewielt \'%1$s\' gëtt et net méi, et kann keng Foto méi dovunner gemaach ginn. + Läsche confirméieren + Läschen + Ofbriechen diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index ef9ed130d4..7e750d175f 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -191,6 +191,7 @@ ਪ੍ਰਾਪਤੀਆਂ ਅੰਕੜੇ ਧੰਨਵਾਦ ਪ੍ਰਾਪਤ ਹੋਏ + ਆਪਣੀਆਂ ਪ੍ਰਾਪਤੀਆਂ ਨੂੰ ਆਪਣੇ ਦੋਸਤਾਂ ਨਾਲ ਸਾਂਝਾ ਕਰੋ! ਸੂਚਨਾਵਾਂ (ਪੜ੍ਹਿਆਂ) ਸੂਚੀ ਅੱਗੇ diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index bfbd64413b..7ac8c92ea6 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -117,6 +117,7 @@ Sërché dle categorìe Arserché j\'element che sò mojen a arpresenta (montagna, Taj Mahal, e via fòrt) Argistré + Mnu dë strabordament Agiorné Lista (Ancor gnun cariament) @@ -781,6 +782,14 @@ An atèisa Falì Impossìbil carié ij dàit dël pòst + Eliminé ëd dossié + Confirmé l\'eliminassion + É-lo sigur ëd vorèj eliminé ël dossié %1$s ch\'a content %2$d element? + Eliminé + Anulé + Ël dossié %1$s a l\'é stàit eliminà për da bin + Impossìbil eliminé ël dossié %1$s + Eror durant l\'eliminassion dël contnù dël dossié: %1$s Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! Ës pòst a l\'ha già dle fòto. An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 370bf0915f..646fb34c0e 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -481,6 +481,7 @@ Du har inga olästa aviseringar Du har inga lästa aviseringar Dela loggar med hjälp av + Kontrollera inkorgen för din e-post Visa lästa Visa olästa Fel uppstod när bilder valdes ut @@ -788,4 +789,7 @@ Pågår Misslyckades Kunde inte läsa in platsdata + Det här platsen har ännu ingen bild. Gå och ta en! + Det här platsen har redan en bild. + Kollar nu om den här platsen har en bild. From 5c8c4032e969d5ba8699ff0c21d1e7bb60ebf890 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Sun, 17 Nov 2024 19:05:25 +0530 Subject: [PATCH 022/231] Migrated util module files from java to kotlin (#5935) * Rename .java to .kt * Migrated the following files in util module to Kotlin - AbstractTextWatcher - ActivityUtils - CommonsDateUtil - DateUtil * Rename .java to .kt * Migrated the following files in util module to Kotlin - DeviceInfoUtil - ExecutorUtils - FragmentUtils --- .../nrw/commons/campaigns/CampaignView.java | 4 +- .../commons/campaigns/CampaignsPresenter.java | 2 +- .../nrw/commons/delete/ReasonBuilder.java | 1 - .../nrw/commons/explore/ExploreFragment.java | 1 - .../commons/media/MediaDetailFragment.java | 1 - .../upload/UploadMediaDetailAdapter.java | 1 - .../UploadMediaDetailFragment.java | 4 +- .../commons/utils/AbstractTextWatcher.java | 31 ------- .../nrw/commons/utils/AbstractTextWatcher.kt | 25 +++++ .../free/nrw/commons/utils/ActivityUtils.java | 15 --- .../free/nrw/commons/utils/ActivityUtils.kt | 16 ++++ .../nrw/commons/utils/CommonsDateUtil.java | 44 --------- .../free/nrw/commons/utils/CommonsDateUtil.kt | 46 ++++++++++ .../fr/free/nrw/commons/utils/DateUtil.java | 53 ----------- .../fr/free/nrw/commons/utils/DateUtil.kt | 62 +++++++++++++ .../nrw/commons/utils/DeviceInfoUtil.java | 91 ------------------- .../free/nrw/commons/utils/DeviceInfoUtil.kt | 80 ++++++++++++++++ .../free/nrw/commons/utils/ExecutorUtils.java | 31 ------- .../free/nrw/commons/utils/ExecutorUtils.kt | 33 +++++++ .../free/nrw/commons/utils/FragmentUtils.java | 15 --- .../free/nrw/commons/utils/FragmentUtils.kt | 20 ++++ .../model/notifications/Notification.java | 2 +- 22 files changed, 288 insertions(+), 290 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java index 4d1eb33ce4..d1ee4c8b06 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java @@ -12,15 +12,15 @@ import fr.free.nrw.commons.campaigns.models.Campaign; import fr.free.nrw.commons.databinding.LayoutCampaginBinding; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DateUtil; +import fr.free.nrw.commons.utils.CommonsDateUtil; +import fr.free.nrw.commons.utils.DateUtil; import java.text.ParseException; import java.util.Date; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.utils.CommonsDateUtil; import fr.free.nrw.commons.utils.SwipableCardView; import fr.free.nrw.commons.utils.ViewUtil; diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java index 51c841451d..157047774e 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java @@ -3,6 +3,7 @@ import android.annotation.SuppressLint; import fr.free.nrw.commons.campaigns.models.Campaign; +import fr.free.nrw.commons.utils.CommonsDateUtil; import java.text.ParseException; import java.util.Collections; import java.util.Date; @@ -14,7 +15,6 @@ import fr.free.nrw.commons.BasePresenter; import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.CommonsDateUtil; import io.reactivex.Scheduler; import io.reactivex.Single; import io.reactivex.SingleObserver; diff --git a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java index 35d6822488..7912375a4d 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java @@ -3,7 +3,6 @@ import android.content.Context; import fr.free.nrw.commons.utils.DateUtil; - import java.util.Date; import java.util.Locale; 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 26c8dd82b2..d444148d4e 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 @@ -11,7 +11,6 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.viewpager.widget.ViewPager.OnPageChangeListener; -import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.R; import fr.free.nrw.commons.ViewPagerAdapter; import fr.free.nrw.commons.contributions.MainActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index ee905a5c56..edfa874fcb 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -55,7 +55,6 @@ import fr.free.nrw.commons.actions.ThanksClient; import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.category.CategoryClient; import fr.free.nrw.commons.category.CategoryDetailsActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java index 6fc8b3266d..a1a639a595 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java @@ -22,7 +22,6 @@ import android.widget.TextView; import androidx.activity.result.ActivityResultLauncher; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.RecyclerView; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java index 5581cfeb1e..fb836445a8 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java @@ -1,7 +1,6 @@ package fr.free.nrw.commons.upload.mediaDetails; import static android.app.Activity.RESULT_OK; -import static fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags; import android.annotation.SuppressLint; import android.app.Activity; @@ -45,6 +44,7 @@ import fr.free.nrw.commons.upload.UploadItem; import fr.free.nrw.commons.upload.UploadMediaDetail; import fr.free.nrw.commons.upload.UploadMediaDetailAdapter; +import fr.free.nrw.commons.utils.ActivityUtils; import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.ImageUtils; import fr.free.nrw.commons.utils.NetworkUtils; @@ -208,7 +208,7 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat try { if(!presenter.getImageQuality(indexOfFragment, inAppPictureLocation, getActivity())) { - startActivityWithFlags( + ActivityUtils.startActivityWithFlags( getActivity(), MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP); } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java deleted file mode 100644 index d5188027d1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.text.Editable; -import android.text.TextWatcher; - -import androidx.annotation.NonNull; - -public class AbstractTextWatcher implements TextWatcher { - private final TextChange textChange; - - public AbstractTextWatcher(@NonNull TextChange textChange) { - this.textChange = textChange; - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - textChange.onTextChanged(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) { - } - - public interface TextChange { - void onTextChanged(String value); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt new file mode 100644 index 0000000000..dd06452f91 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.utils + +import android.text.Editable +import android.text.TextWatcher + +class AbstractTextWatcher( + private val textChange: TextChange +) : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { + // No-op + } + + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { + textChange.onTextChanged(s.toString()) + } + + override fun afterTextChanged(s: Editable?) { + // No-op + } + + interface TextChange { + fun onTextChanged(value: String) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java deleted file mode 100644 index 4806585dc7..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.Intent; - -public class ActivityUtils { - - public static void startActivityWithFlags(Context context, Class cls, int... flags) { - Intent intent = new Intent(context, cls); - for (int flag : flags) { - intent.addFlags(flag); - } - context.startActivity(intent); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt new file mode 100644 index 0000000000..899daaf6b9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ActivityUtils.kt @@ -0,0 +1,16 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.Intent + +object ActivityUtils { + + @JvmStatic + fun startActivityWithFlags(context: Context, cls: Class, vararg flags: Int) { + val intent = Intent(context, cls) + for (flag in flags) { + intent.addFlags(flag) + } + context.startActivity(intent) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java deleted file mode 100644 index 39ddca6838..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.utils; - -import java.text.SimpleDateFormat; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Provides util functions for formatting date time - * Most of our formatting needs are addressed by the data library's DateUtil class - * Methods should be added here only if DateUtil class doesn't provide for it already - */ -public class CommonsDateUtil { - - /** - * Gets SimpleDateFormat for short date pattern - * @return simpledateformat - */ - public static SimpleDateFormat getIso8601DateFormatShort() { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ROOT); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return simpleDateFormat; - } - - /** - * Gets SimpleDateFormat for date pattern returned by Media object - * @return simpledateformat - */ - public static SimpleDateFormat getMediaSimpleDateFormat() { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return simpleDateFormat; - } - - /** - * Gets the timestamp pattern for a date - * @return timestamp - */ - public static SimpleDateFormat getIso8601DateFormatTimestamp() { - final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", - Locale.ROOT); - simpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - return simpleDateFormat; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt new file mode 100644 index 0000000000..c076e19ce1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/CommonsDateUtil.kt @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.utils + +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.TimeZone + +/** + * Provides util functions for formatting date time. + * Most of our formatting needs are addressed by the data library's DateUtil class. + * Methods should be added here only if DateUtil class doesn't provide for it already. + */ +object CommonsDateUtil { + + /** + * Gets SimpleDateFormat for short date pattern. + * @return simpleDateFormat + */ + @JvmStatic + fun getIso8601DateFormatShort(): SimpleDateFormat { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) + simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") + return simpleDateFormat + } + + /** + * Gets SimpleDateFormat for date pattern returned by Media object. + * @return simpleDateFormat + */ + @JvmStatic + fun getMediaSimpleDateFormat(): SimpleDateFormat { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ROOT) + simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") + return simpleDateFormat + } + + /** + * Gets the timestamp pattern for a date. + * @return timestamp + */ + @JvmStatic + fun getIso8601DateFormatTimestamp(): SimpleDateFormat { + val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT) + simpleDateFormat.timeZone = TimeZone.getTimeZone("UTC") + return simpleDateFormat + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java deleted file mode 100644 index 1d2a8fdf7b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.free.nrw.commons.utils; - -import static android.text.format.DateFormat.getBestDateTimePattern; - -import androidx.annotation.NonNull; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -public final class DateUtil { - private static Map DATE_FORMATS = new HashMap<>(); - - // TODO: Switch to DateTimeFormatter when minSdk = 26. - - public static synchronized String iso8601DateFormat(Date date) { - return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date); - } - - public static synchronized Date iso8601DateParse(String date) throws ParseException { - return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date); - } - - public static String getMonthOnlyDateString(@NonNull Date date) { - return getDateStringWithSkeletonPattern(date, "MMMM d"); - } - - public static String getExtraShortDateString(@NonNull Date date) { - return getDateStringWithSkeletonPattern(date, "MMM d"); - } - - public static synchronized String getDateStringWithSkeletonPattern(@NonNull Date date, @NonNull String pattern) { - return getCachedDateFormat(getBestDateTimePattern(Locale.getDefault(), pattern), Locale.getDefault(), false).format(date); - } - - private static SimpleDateFormat getCachedDateFormat(String pattern, Locale locale, boolean utc) { - if (!DATE_FORMATS.containsKey(pattern)) { - SimpleDateFormat df = new SimpleDateFormat(pattern, locale); - if (utc) { - df.setTimeZone(TimeZone.getTimeZone("UTC")); - } - DATE_FORMATS.put(pattern, df); - } - return DATE_FORMATS.get(pattern); - } - - private DateUtil() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt new file mode 100644 index 0000000000..bc33a1edea --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DateUtil.kt @@ -0,0 +1,62 @@ +package fr.free.nrw.commons.utils + +import android.text.format.DateFormat.getBestDateTimePattern +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.HashMap +import java.util.Locale +import java.util.TimeZone + +/** + * Utility class for date formatting and parsing. + * TODO: Switch to DateTimeFormatter when minSdk = 26. + */ +object DateUtil { + + private val DATE_FORMATS: MutableMap = HashMap() + + @JvmStatic + @Synchronized + fun iso8601DateFormat(date: Date): String { + return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).format(date) + } + + @JvmStatic + @Synchronized + @Throws(ParseException::class) + fun iso8601DateParse(date: String): Date { + return getCachedDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ROOT, true).parse(date) + } + + @JvmStatic + fun getMonthOnlyDateString(date: Date): String { + return getDateStringWithSkeletonPattern(date, "MMMM d") + } + + @JvmStatic + fun getExtraShortDateString(date: Date): String { + return getDateStringWithSkeletonPattern(date, "MMM d") + } + + @JvmStatic + @Synchronized + fun getDateStringWithSkeletonPattern(date: Date, pattern: String): String { + return getCachedDateFormat( + getBestDateTimePattern(Locale.getDefault(), pattern), + Locale.getDefault(), false + ).format(date) + } + + @JvmStatic + private fun getCachedDateFormat(pattern: String, locale: Locale, utc: Boolean): SimpleDateFormat { + if (!DATE_FORMATS.containsKey(pattern)) { + val df = SimpleDateFormat(pattern, locale) + if (utc) { + df.timeZone = TimeZone.getTimeZone("UTC") + } + DATE_FORMATS[pattern] = df + } + return DATE_FORMATS[pattern]!! + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java deleted file mode 100644 index 5e01cc606b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.java +++ /dev/null @@ -1,91 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.os.Build; - -import java.util.HashMap; -import java.util.Map; - -import fr.free.nrw.commons.utils.model.ConnectionType; -import fr.free.nrw.commons.utils.model.NetworkConnectionType; - -import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR; -import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR_3G; -import static fr.free.nrw.commons.utils.model.ConnectionType.CELLULAR_4G; -import static fr.free.nrw.commons.utils.model.ConnectionType.NO_INTERNET; -import static fr.free.nrw.commons.utils.model.ConnectionType.WIFI_NETWORK; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.FOUR_G; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.THREE_G; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.TWO_G; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.UNKNOWN; -import static fr.free.nrw.commons.utils.model.NetworkConnectionType.WIFI; - -/** - * Util class to get any information about the user's device - * Ensure that any sensitive information like IMEI is not fetched/shared without user's consent - */ -public class DeviceInfoUtil { - private static final Map TYPE_MAPPING = new HashMap<>(); - - static { - TYPE_MAPPING.put(TWO_G, CELLULAR); - TYPE_MAPPING.put(THREE_G, CELLULAR_3G); - TYPE_MAPPING.put(FOUR_G, CELLULAR_4G); - TYPE_MAPPING.put(WIFI, WIFI_NETWORK); - TYPE_MAPPING.put(UNKNOWN, CELLULAR); - } - - /** - * Get network connection type - * @param context - * @return wifi/cellular-4g/cellular-3g/cellular-2g/no-internet - */ - public static ConnectionType getConnectionType(Context context) { - if (!NetworkUtils.isInternetConnectionEstablished(context)) { - return NO_INTERNET; - } - NetworkConnectionType networkType = NetworkUtils.getNetworkType(context); - ConnectionType deviceNetworkType = TYPE_MAPPING.get(networkType); - return deviceNetworkType == null ? CELLULAR : deviceNetworkType; - } - - /** - * Get Device manufacturer - * @return - */ - public static String getDeviceManufacturer() { - return Build.MANUFACTURER; - } - - /** - * Get Device model name - * @return - */ - public static String getDeviceModel() { - return Build.MODEL; - } - - /** - * Get Android version. Eg. 4.4.2 - * @return - */ - public static String getAndroidVersion() { - return Build.VERSION.RELEASE; - } - - /** - * Get API Level. Eg. 26 - * @return - */ - public static String getAPILevel() { - return Build.VERSION.SDK; - } - - /** - * Get Device. - * @return - */ - public static String getDevice() { - return Build.DEVICE; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt new file mode 100644 index 0000000000..05d71c7e19 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/DeviceInfoUtil.kt @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.os.Build +import fr.free.nrw.commons.utils.model.ConnectionType +import fr.free.nrw.commons.utils.model.NetworkConnectionType + +/** + * Util class to get any information about the user's device + * Ensure that any sensitive information like IMEI is not fetched/shared without user's consent + */ +object DeviceInfoUtil { + private val TYPE_MAPPING = mapOf( + NetworkConnectionType.TWO_G to ConnectionType.CELLULAR, + NetworkConnectionType.THREE_G to ConnectionType.CELLULAR_3G, + NetworkConnectionType.FOUR_G to ConnectionType.CELLULAR_4G, + NetworkConnectionType.WIFI to ConnectionType.WIFI_NETWORK, + NetworkConnectionType.UNKNOWN to ConnectionType.CELLULAR + ) + + /** + * Get network connection type + * @param context + * @return wifi/cellular-4g/cellular-3g/cellular-2g/no-internet + */ + @JvmStatic + fun getConnectionType(context: Context): ConnectionType { + return if (!NetworkUtils.isInternetConnectionEstablished(context)) { + ConnectionType.NO_INTERNET + } else { + val networkType = NetworkUtils.getNetworkType(context) + TYPE_MAPPING[networkType] ?: ConnectionType.CELLULAR + } + } + + /** + * Get Device manufacturer + * @return + */ + @JvmStatic + fun getDeviceManufacturer(): String { + return Build.MANUFACTURER + } + + /** + * Get Device model name + * @return + */ + @JvmStatic + fun getDeviceModel(): String { + return Build.MODEL + } + + /** + * Get Android version. Eg. 4.4.2 + * @return + */ + @JvmStatic + fun getAndroidVersion(): String { + return Build.VERSION.RELEASE + } + + /** + * Get API Level. Eg. 26 + * @return + */ + @JvmStatic + fun getAPILevel(): String { + return Build.VERSION.SDK + } + + /** + * Get Device. + * @return + */ + @JvmStatic + fun getDevice(): String { + return Build.DEVICE + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java deleted file mode 100644 index 889b31f2d1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Handler; -import android.os.Looper; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -public class ExecutorUtils { - - private static final Executor uiExecutor = command -> { - if (Looper.myLooper() == Looper.getMainLooper()) { - command.run(); - } else { - new Handler(Looper.getMainLooper()).post(command); - } - }; - - public static Executor uiExecutor() { - return uiExecutor; - } - - - private static final ExecutorService executor = Executors.newFixedThreadPool(3); - - public static ExecutorService get() { - return executor; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt new file mode 100644 index 0000000000..981b193552 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ExecutorUtils.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.utils + +import android.os.Handler +import android.os.Looper + +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +object ExecutorUtils { + + @JvmStatic + private val uiExecutor: Executor = Executor { command -> + if (Looper.myLooper() == Looper.getMainLooper()) { + command.run() + } else { + Handler(Looper.getMainLooper()).post(command) + } + } + + @JvmStatic + fun uiExecutor(): Executor { + return uiExecutor + } + + @JvmStatic + private val executor: ExecutorService = Executors.newFixedThreadPool(3) + + @JvmStatic + fun get(): ExecutorService { + return executor + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java deleted file mode 100644 index a01ff92512..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.java +++ /dev/null @@ -1,15 +0,0 @@ -package fr.free.nrw.commons.utils; - -import androidx.fragment.app.Fragment; - -public class FragmentUtils { - - /** - * Utility function to check whether the fragment UI is still active or not - * @param fragment - * @return - */ - public static boolean isFragmentUIActive(Fragment fragment) { - return fragment!=null && fragment.getActivity() != null && fragment.isAdded() && !fragment.isDetached() && !fragment.isRemoving(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt new file mode 100644 index 0000000000..4cdeecda2a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/FragmentUtils.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.utils + +import androidx.fragment.app.Fragment + +object FragmentUtils { + + /** + * Utility function to check whether the fragment UI is still active or not + * @param fragment + * @return Boolean + */ + @JvmStatic + fun isFragmentUIActive(fragment: Fragment?): Boolean { + return fragment != null && + fragment.activity != null && + fragment.isAdded && + !fragment.isDetached && + !fragment.isRemoving + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java index 2b18669a49..2d1dbdf28a 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java @@ -7,8 +7,8 @@ import com.google.gson.JsonObject; import com.google.gson.annotations.SerializedName; -import org.apache.commons.lang3.StringUtils; import fr.free.nrw.commons.utils.DateUtil; +import org.apache.commons.lang3.StringUtils; import fr.free.nrw.commons.wikidata.GsonUtil; import java.text.ParseException; From c439143dd31c5c3d42f71de0fb4e4e51ba9fe4f6 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 18 Nov 2024 13:01:52 +0100 Subject: [PATCH 023/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-az/strings.xml | 2 ++ app/src/main/res/values-de/strings.xml | 13 +++++++++++++ app/src/main/res/values-io/strings.xml | 1 + app/src/main/res/values-iw/strings.xml | 6 +++--- app/src/main/res/values-krc/strings.xml | 10 ++++++++++ app/src/main/res/values-mk/strings.xml | 10 ++++++++++ app/src/main/res/values-nl/strings.xml | 10 ++++++++++ app/src/main/res/values-pa/strings.xml | 3 +++ app/src/main/res/values-pms/strings.xml | 1 + app/src/main/res/values-skr/strings.xml | 2 ++ app/src/main/res/values-zh/strings.xml | 12 ++++++++++++ 11 files changed, 67 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 1edbe43fce..d97d5ed208 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -10,6 +10,7 @@ * Toghrul Rahimli * Wertuose * Şeyx Şamil +* Əkrəm Cəfər --> Commons Facebook səhifəsi @@ -124,4 +125,5 @@ Bildiriş oxunmuş olaraq işarələndi Zəhmət olmasa tətbiqin cari məkanınızı göstərmək üçün məkan xidmətlərini aktiv edin Yaxınlıqdakı şəkilləri göstərmək üçün məkan icazəsi lazımdır + Qovluğu Sil diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a6471c1fe9..215ecc9855 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -19,6 +19,7 @@ * ManuelFranz * Mcliquid * Metalhead64 +* Mukeber * Nekky-chan * Ngschaider * Pyscowicz @@ -512,6 +513,7 @@ Du hast keine ungelesenen Benachrichtigungen Du hast keine gelesenen Benachrichtigungen Protokolle freigeben mit + Überprüfe deinen E-Mail-Posteingang Gelesene ansehen Ungelesene ansehen Beim Auswählen der Bilder ist ein Fehler aufgetreten @@ -816,4 +818,15 @@ Ausstehend Fehlgeschlagen Ortsdaten konnten nicht geladen werden + Ordner löschen + Löschung bestätigen + Bist du sicher, dass du den Ordner %1$s löschen möchten, die %2$d Datenobjekte enthalten? + Löschen + Abbrechen + Ordner %1$s erfolgreich gelöscht + Ordner %1$s konnte nicht gelöscht werden + Fehler beim Löschen des Ordnerinhalts: %1$s + Von diesem Ort gibt es noch kein Bild. Mach eins! + Dieser Ort hat bereits ein Bild. + Jetzt wird geprüft, ob dieser Ort bereits ein Bild hat. diff --git a/app/src/main/res/values-io/strings.xml b/app/src/main/res/values-io/strings.xml index 51fe164419..9a9f7d70e6 100644 --- a/app/src/main/res/values-io/strings.xml +++ b/app/src/main/res/values-io/strings.xml @@ -525,6 +525,7 @@ SAVEZ PLUSE Bezonas permiso Vidar uzeropagino + Ka vu deziras informar la loko de ube vu obtenis ca imajo?\nInformo pri la lokizo helpos editeri trovar vua imajo, do ol divenos plu utila.\nDanko! Imajo selektita Ca imajo indikesis por ne sendesar Raporto diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 3389a227e3..2ea71f748a 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -114,7 +114,7 @@ ההעלאה התחילה! ההעלאות בתור (מופעל מצב חיבור מוגבל) הקובץ %1$s הועלה! - ללחוץ כאן כדי לצפות בהעלאה שלך + נא ללחוץ כאן כדי לצפות בהעלאה שלך העלאת קובץ: %s מתבצעת העלאת %1$s העלאת %1$s מסתיימת @@ -123,7 +123,7 @@ לחץ כדי להציג יש לגעת כדי לראות ההעלאות האחרונות שלי - בתור + הוכנסה בתור נכשלה %1$d%% הושלמו העלאה @@ -674,7 +674,7 @@ ההעלאה מושהית… ביטול ההעלאה... ביטול ההעלאה - הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. + הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות והן תימשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. נא לכתוב תיאור קצר שמסביר מה מופיע בתמונה. בתיאור, כדאי לכתוב מה הופך את התמונה הזאת למעניינת, טיפוסית או נדירה ולהסביר את ההקשר, בין אם גלוי או סמוי. יש להשתמש במינוח מדויק ככל הניתן. נא למצוא ולבחור את כל העקרונות שהתמונה הזאת מתארת. נא לשמור על דיוק מרבי. אם התמונה מתארת מגוון פריטים, נא לבחור אותם בגבולות הסביר. לא לבחור תגיות גנריות אם יש תגיות יותר נקודתית זמינות. diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index eb9193e7f0..13de1bb124 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -119,6 +119,7 @@ Категорияланы изле Медиагъызда суратланнган элементлени излегиз (тау, Тадж Махал э. а. к.) Сакъландыр + Къошакъ меню Джангырт Тизме (Алкъын джюклеуле джокъдула) @@ -781,6 +782,15 @@ Сакълауда Джетишимсиз Джерни юсюнден билгилени джюклеялмады + Папканы кетер + Кетериуню мюкюл эт + %2$d элементи болгъан %1$s папканы кетерирге излегенинге ишексизмисе? + Кетер + Ызына ал + %1$s папка джетишимли кетерилгенди + %1$s папканы кетериу джетишимисизди + Папканы ичиндегисини кетериуде халат: %1$s + Контейнерни идентификатору папкагъа джол табалмады: %1$d Бу джерни сураты джокъду, хайда бирин эт! Бу джерни алайсыз да сураты барды. Бу джерни сураты болуб-болмагъанын тинте турама. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 916f4f4202..61d71ea683 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -119,6 +119,7 @@ Пребарај категории Пребарајте ги предметите прикажани на сликата или снимката (планина, Лесновски манастир итн.) Зачувај + Преливно мени Превчитај Список (Сè уште нема подигања) @@ -785,6 +786,15 @@ Во исчекување Неуспешно Не можев да ги вчитам податоците за место + Избриши папка + Потврдете бришење + Дали сигурно сакате да ја избришете папката %1$s, која содржи %2$d ставки? + Избриши + Откажи + Папката %1$s е успешно избришана + Не успеав да ја избришам папката %1$s + Не можев да ја префрлам содржината на папката во ѓубре: %1$s + Не успеав да ја добијам патеката на папката за групата со назнака: %1$d Местово сè уште нема слика. Направете ја! Местово веќе има слика. Проверувам дали местово има слика. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 8cc3553a17..bc30166667 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -140,6 +140,7 @@ Categorieën zoeken Objecten zoeken die in uw bestand worden weergeven (berg, Taj Mahal, enz.) Opslaan + Overloopmenu Vernieuwen Lijst (Nog geen uploads) @@ -806,6 +807,15 @@ In behandeling Mislukt Plaatsgegevens konden niet geladen worden + Map verwijderen + Bevestig verwijdering + Weet u zeker dat u de map %1$s met %2$d onderdelen wilt verwijderen? + Verwijderen + Annuleren + De map %1$s is verwijderd + Het is niet gelukt de map %1$s te verwijderen + Fout bij het weggooien van de inhoud van de map: %1$s + Het is niet gelukt om het mappad voor bucket-ID %1$d op te halen Er is nog geen foto van deze plek, maak er eentje! Er is al een foto van deze plek. We controleren nu of er een foto van deze plek is. diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 7e750d175f..2733093d48 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -168,7 +168,9 @@ ਵਿਕੀਪੀਡੀਆ ਲੇਖ ਤਸਵੀਰ ਬਹੁਤ ਗੂੜ੍ਹੀ ਹੈ। ਤਸਵੀਰ ਧੁੰਦਲੀ ਹੈ। + ਆਪਣੇ ਖਾਤੇ ਵਿੱਚ ਦਾਖ਼ਲ ਹੋਵੋ ਛੱਡੋ + ਦਾਖ਼ਲ ਹੋਵੋ ਵਿਕੀਡੇਟਾ ਵਿਕੀਪੀਡੀਆ ਅਕਸਰ ਪੁੱਛੇ ਜਾਂਦੇ ਸੁਆਲ @@ -185,6 +187,7 @@ ਸ਼੍ਰੇਣੀਆਂ ਨਕਸ਼ਾ ਸਵਾਲ + ਤੁਹਾਡੇ ਦਾਖਲੇ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਮੁੜ ਦਾਖਲ ਹੋਵੋ। ਜਾਰੀ ਰੱਖੋ ਕੋਈ ਤਾਜ਼ਾ ਖੋਜ ਨਹੀਂ ਮਿਟਾਓ diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 7ac8c92ea6..2ccd3c07a7 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -790,6 +790,7 @@ Ël dossié %1$s a l\'é stàit eliminà për da bin Impossìbil eliminé ël dossié %1$s Eror durant l\'eliminassion dël contnù dël dossié: %1$s + Falì a arcuperé ël sënté d\'acess al dossié për ël sigilin d\'ID: %1$d Ës pòst a l\'ha ancor gnun-e fòto, ch\'a na pija un-a! Ës pòst a l\'ha già dle fòto. An camin ch\'as verìfica si cost pòst -sì a l\'ha dle fòto. diff --git a/app/src/main/res/values-skr/strings.xml b/app/src/main/res/values-skr/strings.xml index 4c68ee91b0..f36e6f9835 100644 --- a/app/src/main/res/values-skr/strings.xml +++ b/app/src/main/res/values-skr/strings.xml @@ -272,4 +272,6 @@ اپلوڈاں وچار ہیٹھ ناکام تھیا + مٹاؤ + منسوخ diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 2adabec26e..1ca6cc047d 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -34,11 +34,13 @@ * SomeyaMako * Stang * StarrySky +* TFX202X * Tranve * U.T. * Vikarna * VulpesVulpes825 * Whym +* WiiUf * Willy1018 * Wxyveronica * XiaoGuoQuQ233 @@ -170,6 +172,7 @@ 搜索分类 搜索您的媒体描述的项目(如山、泰姬陵等) 保存 + 溢出菜单 刷新 列表 (尚无上传) @@ -835,6 +838,15 @@ 待处理 失败 无法加载地点数据 + 删除文件夹 + 确认删除 + 您确定要删除包含%2$d的文件夹%1$s吗? + 删除 + 撤消 + 文件夹%1$s已成功删除 + 无法删除文件夹%1$s + 删除文件夹内容时出错: %1$s + 无法检索存储桶ID的文件夹路径: %1$d 这个地点还没有照片,快去拍一张吧! 这个地点已经有照片了。 现在检查这个地点是否有照片。 From 0fdb0044b96041e0eed0262f49de02db2d4047a5 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Mon, 18 Nov 2024 19:10:35 +0530 Subject: [PATCH 024/231] Migrated util module from Java to Kotlin (#5938) * Rename .java to .kt * Migrated the following files in util module to Kotlin - AbstractTextWatcher - ActivityUtils - CommonsDateUtil - DateUtil * Rename .java to .kt * Migrated the following files in util module to Kotlin - DeviceInfoUtil - ExecutorUtils - FragmentUtils * Rename .java to .kt * Migrated the following files in util module to Kotlin - ImageUtils - ImageUtilsWrapper - LangCodeUtils - LayoutUtils - LengthUtils - LocationUtils - MapUtils * Rename .java to .kt * Migrated all remaining files in util module --- .../contributions/ContributionController.java | 4 +- .../contributions/ContributionsFragment.java | 4 - .../commons/contributions/MainActivity.java | 6 - .../free/nrw/commons/delete/DeleteHelper.java | 3 +- .../explore/map/ExploreMapFragment.java | 13 +- .../commons/media/MediaDetailFragment.java | 4 +- .../fragments/NearbyParentFragment.java | 7 +- .../NearbyParentFragmentPresenter.java | 7 - .../commons/settings/SettingsFragment.java | 5 +- .../nrw/commons/upload/UploadActivity.java | 7 +- .../fr/free/nrw/commons/utils/ImageUtils.java | 351 ----------------- .../fr/free/nrw/commons/utils/ImageUtils.kt | 363 ++++++++++++++++++ .../nrw/commons/utils/ImageUtilsWrapper.java | 30 -- .../nrw/commons/utils/ImageUtilsWrapper.kt | 29 ++ .../free/nrw/commons/utils/LangCodeUtils.java | 39 -- .../free/nrw/commons/utils/LangCodeUtils.kt | 40 ++ .../free/nrw/commons/utils/LayoutUtils.java | 38 -- .../fr/free/nrw/commons/utils/LayoutUtils.kt | 47 +++ .../free/nrw/commons/utils/LengthUtils.java | 145 ------- .../fr/free/nrw/commons/utils/LengthUtils.kt | 156 ++++++++ .../free/nrw/commons/utils/LocationUtils.java | 58 --- .../free/nrw/commons/utils/LocationUtils.kt | 63 +++ .../fr/free/nrw/commons/utils/MapUtils.java | 33 -- .../fr/free/nrw/commons/utils/MapUtils.kt | 39 ++ .../commons/utils/MediaDataExtractorUtil.java | 29 -- .../commons/utils/MediaDataExtractorUtil.kt | 29 ++ .../nrw/commons/utils/NearbyFABUtils.java | 51 --- .../free/nrw/commons/utils/NearbyFABUtils.kt | 55 +++ .../free/nrw/commons/utils/NetworkUtils.java | 94 ----- .../fr/free/nrw/commons/utils/NetworkUtils.kt | 85 ++++ .../nrw/commons/utils/PermissionUtils.java | 224 ----------- .../free/nrw/commons/utils/PermissionUtils.kt | 231 +++++++++++ .../fr/free/nrw/commons/utils/PlaceUtils.java | 55 --- .../fr/free/nrw/commons/utils/PlaceUtils.kt | 50 +++ .../nrw/commons/utils/StringSortingUtils.java | 90 ----- .../nrw/commons/utils/StringSortingUtils.kt | 86 +++++ .../fr/free/nrw/commons/utils/StringUtil.java | 38 -- .../fr/free/nrw/commons/utils/StringUtil.kt | 37 ++ .../nrw/commons/utils/SwipableCardView.java | 74 ---- .../nrw/commons/utils/SwipableCardView.kt | 64 +++ .../nrw/commons/utils/SystemThemeUtils.java | 49 --- .../nrw/commons/utils/SystemThemeUtils.kt | 52 +++ .../fr/free/nrw/commons/utils/UiUtils.java | 39 -- .../java/fr/free/nrw/commons/utils/UiUtils.kt | 41 ++ .../fr/free/nrw/commons/utils/ViewUtil.java | 143 ------- .../fr/free/nrw/commons/utils/ViewUtil.kt | 151 ++++++++ .../nrw/commons/utils/ViewUtilWrapper.java | 23 -- .../free/nrw/commons/utils/ViewUtilWrapper.kt | 17 + 48 files changed, 1651 insertions(+), 1647 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index fcfd329740..e910799d07 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -87,7 +87,7 @@ public void initiateCameraPick(Activity activity, }, R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } /** @@ -224,7 +224,7 @@ public void initiateCustomGalleryPickWithPermission(final Activity activity, Act () -> FilePicker.openCustomSelector(activity, resultLauncher, 0), R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index a840aa8e11..1699f35f0e 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -5,7 +5,6 @@ import static fr.free.nrw.commons.contributions.Contribution.STATE_PAUSED; import static fr.free.nrw.commons.nearby.fragments.NearbyParentFragment.WLM_URL; import static fr.free.nrw.commons.profile.ProfileActivity.KEY_USERNAME; -import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import static fr.free.nrw.commons.utils.LengthUtils.computeBearing; import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween; @@ -23,12 +22,10 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.MenuItem.OnMenuItemClickListener; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; import android.widget.ImageView; -import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; @@ -39,7 +36,6 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager.OnBackStackChangedListener; import androidx.fragment.app.FragmentTransaction; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.databinding.FragmentContributionsBinding; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index a9e9ee5c62..849ef3450b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -1,13 +1,10 @@ package fr.free.nrw.commons.contributions; -import android.Manifest.permission; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -16,10 +13,8 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; import fr.free.nrw.commons.databinding.MainBinding; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -41,7 +36,6 @@ import fr.free.nrw.commons.quiz.QuizChecker; import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.upload.UploadActivity; import fr.free.nrw.commons.upload.UploadProgressActivity; import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.PermissionUtils; diff --git a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java index 02f7d418e1..134ee48d9d 100644 --- a/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/delete/DeleteHelper.java @@ -1,10 +1,10 @@ package fr.free.nrw.commons.delete; import static fr.free.nrw.commons.notification.NotificationHelper.NOTIFICATION_DELETE; +import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.annotation.SuppressLint; import android.content.Context; -import static fr.free.nrw.commons.utils.LangCodeUtils.getLocalizedResources; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; @@ -16,6 +16,7 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.notification.NotificationHelper; import fr.free.nrw.commons.review.ReviewController; +import fr.free.nrw.commons.utils.LangCodeUtils; import fr.free.nrw.commons.utils.ViewUtilWrapper; import io.reactivex.Observable; import io.reactivex.Single; 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 52a5571e94..441f46e610 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 @@ -4,14 +4,12 @@ import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED; import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; -import android.Manifest; import android.Manifest.permission; import android.annotation.SuppressLint; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Paint; @@ -21,22 +19,17 @@ import android.location.LocationManager; import android.os.Bundle; import android.preference.PreferenceManager; -import android.provider.Settings; import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.Toast; -import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.appcompat.widget.AppCompatTextView; import androidx.core.content.ContextCompat; import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.snackbar.Snackbar; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.MapController; @@ -48,7 +41,6 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.explore.ExploreMapRootFragment; import fr.free.nrw.commons.explore.paging.LiveDataConverter; -import fr.free.nrw.commons.filepicker.Constants; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationPermissionsHelper; @@ -60,7 +52,6 @@ import fr.free.nrw.commons.utils.DialogUtil; import fr.free.nrw.commons.utils.MapUtils; import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.PermissionUtils; import fr.free.nrw.commons.utils.SystemThemeUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; @@ -310,7 +301,7 @@ private void unregisterNetworkReceiver() { } private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); @@ -331,7 +322,7 @@ private void performMapReadyActions() { !locationPermissionsHelper.checkLocationPermission(getActivity())) { isPermissionDenied = true; } - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index edfa874fcb..ed20809acd 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -318,7 +318,7 @@ public void run() { } public void launchZoomActivity(final View view) { - final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE); + final boolean hasPermission = PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE()); if (hasPermission) { launchZoomActivityAfterPermissionCheck(view); } else { @@ -328,7 +328,7 @@ public void launchZoomActivity(final View view) { }, R.string.storage_permission_title, R.string.read_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE + PermissionUtils.getPERMISSIONS_STORAGE() ); } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 6a2e5c3a9e..fdbc727bc6 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -43,7 +43,6 @@ import android.view.ViewGroup.LayoutParams; import android.view.animation.Animation; import android.view.animation.AnimationUtils; -import android.widget.Button; import android.widget.Toast; import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; @@ -701,7 +700,7 @@ private void startMapWithoutPermission() { = new LatLng(Double.parseDouble(locationLatLng[0]), Double.parseDouble(locationLatLng[1]), 1f); } else { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); } if (binding.map != null) { moveCameraToPosition( @@ -793,7 +792,7 @@ public void initNearbyFilter() { hideBottomSheet(); binding.nearbyFilter.searchViewLayout.searchView.setOnQueryTextFocusChangeListener( (v, hasFocus) -> { - LayoutUtils.setLayoutHeightAllignedToWidth(1.25, + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); if (hasFocus) { binding.nearbyFilterList.getRoot().setVisibility(View.VISIBLE); @@ -834,7 +833,7 @@ public boolean isDarkTheme() { .getLayoutParams().width = (int) LayoutUtils.getScreenWidth(getActivity(), 0.75); binding.nearbyFilterList.searchListView.setAdapter(nearbyFilterSearchRecyclerViewAdapter); - LayoutUtils.setLayoutHeightAllignedToWidth(1.25, binding.nearbyFilterList.getRoot()); + LayoutUtils.setLayoutHeightAlignedToWidth(1.25, binding.nearbyFilterList.getRoot()); compositeDisposable.add( RxSearchView.queryTextChanges(binding.nearbyFilter.searchViewLayout.searchView) .takeUntil(RxView.detaches(binding.nearbyFilter.searchViewLayout.searchView)) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java index 410aeb9f40..00a491e689 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/presenter/NearbyParentFragmentPresenter.java @@ -11,13 +11,10 @@ import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import android.location.Location; -import android.view.View; import androidx.annotation.MainThread; import androidx.annotation.Nullable; -import androidx.work.ExistingWorkPolicy; import fr.free.nrw.commons.BaseMarker; import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao; -import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.kvstore.JsonKvStore; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; @@ -26,14 +23,10 @@ import fr.free.nrw.commons.nearby.Label; import fr.free.nrw.commons.nearby.MarkerPlaceGroup; import fr.free.nrw.commons.nearby.NearbyController; -import fr.free.nrw.commons.nearby.NearbyFilterState; import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.PlaceDao; import fr.free.nrw.commons.nearby.contract.NearbyParentFragmentContract; -import fr.free.nrw.commons.upload.worker.WorkRequestHelper; import fr.free.nrw.commons.utils.LocationUtils; import fr.free.nrw.commons.wikidata.WikidataEditListener; -import io.reactivex.disposables.CompositeDisposable; import java.lang.reflect.Proxy; import java.util.List; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 20fc831a84..d4ed379f09 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -12,7 +12,6 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; -import android.util.Log; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemClickListener; @@ -543,7 +542,7 @@ private String getCurrentLanguageCode(final String preferenceKey) { * First checks for external storage permissions and then sends logs via email */ private void checkPermissionsAndSendLogs() { - if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.PERMISSIONS_STORAGE)) { + if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE())) { commonsLogSender.send(getActivity(), null); } else { requestExternalStoragePermissions(); @@ -556,7 +555,7 @@ private void checkPermissionsAndSendLogs() { */ private void requestExternalStoragePermissions() { Dexter.withActivity(getActivity()) - .withPermissions(PermissionUtils.PERMISSIONS_STORAGE) + .withPermissions(PermissionUtils.getPERMISSIONS_STORAGE()) .withListener(new MultiplePermissionsListener() { @Override public void onPermissionsChecked(MultiplePermissionsReport report) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java index 35906c3fb5..ed65b05dff 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java @@ -1,8 +1,8 @@ package fr.free.nrw.commons.upload; import static fr.free.nrw.commons.contributions.ContributionController.ACTION_INTERNAL_UPLOADS; -import static fr.free.nrw.commons.utils.PermissionUtils.PERMISSIONS_STORAGE; import static fr.free.nrw.commons.utils.PermissionUtils.checkPermissionsAndPerformAction; +import static fr.free.nrw.commons.utils.PermissionUtils.getPERMISSIONS_STORAGE; import static fr.free.nrw.commons.wikidata.WikidataConstants.PLACE_OBJECT; import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE; import static fr.free.nrw.commons.wikidata.WikidataConstants.SELECTED_NEARBY_PLACE_CATEGORY; @@ -32,7 +32,6 @@ import androidx.viewpager.widget.PagerAdapter; import androidx.viewpager.widget.ViewPager; import androidx.work.ExistingWorkPolicy; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SessionManager; @@ -277,7 +276,7 @@ protected void checkBlockStatus() { public void checkStoragePermissions() { // Check if all required permissions are granted - final boolean hasAllPermissions = PermissionUtils.hasPermission(this, PERMISSIONS_STORAGE); + final boolean hasAllPermissions = PermissionUtils.hasPermission(this, getPERMISSIONS_STORAGE()); final boolean hasPartialAccess = PermissionUtils.hasPartialAccess(this); if (hasAllPermissions || hasPartialAccess) { // All required permissions are granted, so enable UI elements and perform actions @@ -297,7 +296,7 @@ public void checkStoragePermissions() { }, R.string.storage_permission_title, R.string.write_storage_permission_rationale_for_image_share, - PERMISSIONS_STORAGE); + getPERMISSIONS_STORAGE()); } } /* If all permissions are not granted and a dialog is already showing on screen diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java deleted file mode 100644 index 99155a5e3b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ /dev/null @@ -1,351 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.ProgressDialog; -import android.app.WallpaperManager; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Color; -import android.net.Uri; -import android.os.Build; -import androidx.annotation.IntDef; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; -import androidx.exifinterface.media.ExifInterface; -import androidx.work.Data; -import androidx.work.OneTimeWorkRequest; -import androidx.work.WorkManager; -import com.facebook.common.executors.CallerThreadExecutor; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.SetWallpaperWorker; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import timber.log.Timber; - -/** - * Created by bluesir9 on 3/10/17. - */ - -public class ImageUtils { - - /** - * Set 0th bit as 1 for dark image ie. 0001 - */ - public static final int IMAGE_DARK = 1 << 0; // 1 - /** - * Set 1st bit as 1 for blurry image ie. 0010 - */ - public static final int IMAGE_BLURRY = 1 << 1; // 2 - /** - * Set 2nd bit as 1 for duplicate image ie. 0100 - */ - public static final int IMAGE_DUPLICATE = 1 << 2; //4 - /** - * Set 3rd bit as 1 for image with different geo location ie. 1000 - */ - public static final int IMAGE_GEOLOCATION_DIFFERENT = 1 << 3; //8 - /** - * The parameter FILE_FBMD is returned from the class ReadFBMD if the uploaded image contains FBMD data else returns IMAGE_OK - * ie. 10000 - */ - public static final int FILE_FBMD = 1 << 4; - /** - * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does not contains EXIF data else returns IMAGE_OK - * ie. 100000 - */ - public static final int FILE_NO_EXIF = 1 << 5; - public static final int IMAGE_OK = 0; - public static final int IMAGE_KEEP = -1; - public static final int IMAGE_WAIT = -2; - public static final int EMPTY_CAPTION = -3; - public static final int FILE_NAME_EXISTS = 1 << 6; - static final int NO_CATEGORY_SELECTED = -5; - - private static ProgressDialog progressDialogWallpaper; - - private static ProgressDialog progressDialogAvatar; - - @IntDef( - flag = true, - value = { - IMAGE_DARK, - IMAGE_BLURRY, - IMAGE_DUPLICATE, - IMAGE_OK, - IMAGE_KEEP, - IMAGE_WAIT, - EMPTY_CAPTION, - FILE_NAME_EXISTS, - NO_CATEGORY_SELECTED, - IMAGE_GEOLOCATION_DIFFERENT - } - ) - @Retention(RetentionPolicy.SOURCE) - public @interface Result { - } - - /** - * @return IMAGE_OK if image is not too dark - * IMAGE_DARK if image is too dark - */ - static @Result int checkIfImageIsTooDark(String imagePath) { - long millis = System.currentTimeMillis(); - try { - Bitmap bmp = new ExifInterface(imagePath).getThumbnailBitmap(); - if (bmp == null) { - bmp = BitmapFactory.decodeFile(imagePath); - } - - if (checkIfImageIsDark(bmp)) { - return IMAGE_DARK; - } - - } catch (Exception e) { - Timber.d(e, "Error while checking image darkness."); - } finally { - Timber.d("Checking image darkness took " + (System.currentTimeMillis() - millis) + " ms."); - } - return IMAGE_OK; - } - - /** - * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will be an empty string - * @param latLng Location of wikidata item will be edited after upload - * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null - * true if geolocation of the image and wikidata item are different - */ - static boolean checkImageGeolocationIsDifferent(String geolocationOfFileString, LatLng latLng) { - Timber.d("Comparing geolocation of file with nearby place location"); - if (latLng == null) { // Means that geolocation for this image is not given - return false; // Since we don't know geolocation of file, we choose letting upload - } - - String[] geolocationOfFile = geolocationOfFileString.split("\\|"); - Double distance = LengthUtils.computeDistanceBetween( - new LatLng(Double.parseDouble(geolocationOfFile[0]),Double.parseDouble(geolocationOfFile[1]),0) - , latLng); - // Distance is more than 1 km, means that geolocation is wrong - return distance >= 1000; - } - - private static boolean checkIfImageIsDark(Bitmap bitmap) { - if (bitmap == null) { - Timber.e("Expected bitmap was null"); - return true; - } - - int bitmapWidth = bitmap.getWidth(); - int bitmapHeight = bitmap.getHeight(); - - int allPixelsCount = bitmapWidth * bitmapHeight; - int numberOfBrightPixels = 0; - int numberOfMediumBrightnessPixels = 0; - double brightPixelThreshold = 0.025 * allPixelsCount; - double mediumBrightPixelThreshold = 0.3 * allPixelsCount; - - for (int x = 0; x < bitmapWidth; x++) { - for (int y = 0; y < bitmapHeight; y++) { - int pixel = bitmap.getPixel(x, y); - int r = Color.red(pixel); - int g = Color.green(pixel); - int b = Color.blue(pixel); - - int secondMax = r > g ? r : g; - double max = (secondMax > b ? secondMax : b) / 255.0; - - int secondMin = r < g ? r : g; - double min = (secondMin < b ? secondMin : b) / 255.0; - - double luminance = ((max + min) / 2.0) * 100; - - int highBrightnessLuminance = 40; - int mediumBrightnessLuminance = 26; - - if (luminance < highBrightnessLuminance) { - if (luminance > mediumBrightnessLuminance) { - numberOfMediumBrightnessPixels++; - } - } else { - numberOfBrightPixels++; - } - - if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) { - return false; - } - } - } - return true; - } - - /** - * Downloads the image from the URL and sets it as the phone's wallpaper - * Fails silently if download or setting wallpaper fails. - * - * @param context context - * @param imageUrl Url of the image - */ - public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { - - enqueueSetWallpaperWork(context, imageUrl); - - } - - private static void createNotificationChannel(Context context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - CharSequence name = "Wallpaper Setting"; - String description = "Notifications for wallpaper setting progress"; - int importance = NotificationManager.IMPORTANCE_DEFAULT; - NotificationChannel channel = new NotificationChannel("set_wallpaper_channel", name, importance); - channel.setDescription(description); - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - notificationManager.createNotificationChannel(channel); - } - } - - - /** - * Calls the set avatar api to set the image url as user's avatar - * @param context - * @param url - * @param username - * @param okHttpJsonApiClient - * @param compositeDisposable - */ - public static void setAvatarFromImageUrl(Context context, String url, String username, - OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable) { - showSettingAvatarProgressBar(context); - - try { - compositeDisposable.add(okHttpJsonApiClient - .setAvatar(username, url) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null && response.getStatus().equals("200")) { - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully)); - if (progressDialogAvatar != null && progressDialogAvatar.isShowing()) { - progressDialogAvatar.dismiss(); - } - } - }, - t -> { - Timber.e(t, "Setting Avatar Failed"); - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)); - if (progressDialogAvatar != null) { - progressDialogAvatar.cancel(); - } - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)); - if (progressDialogAvatar != null) { - progressDialogAvatar.cancel(); - } - } - - } - - public static void enqueueSetWallpaperWork(Context context, Uri imageUrl) { - createNotificationChannel(context); // Ensure the notification channel is created - - Data inputData = new Data.Builder() - .putString("imageUrl", imageUrl.toString()) - .build(); - - OneTimeWorkRequest setWallpaperWork = new OneTimeWorkRequest.Builder(SetWallpaperWorker.class) - .setInputData(inputData) - .build(); - - WorkManager.getInstance(context).enqueue(setWallpaperWork); - } - - - private static void showSettingWallpaperProgressBar(Context context) { - progressDialogWallpaper = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title), - context.getString(R.string.setting_wallpaper_dialog_message), true); - } - - private static void showSettingAvatarProgressBar(Context context) { - progressDialogAvatar = ProgressDialog.show(context, context.getString(R.string.setting_avatar_dialog_title), - context.getString(R.string.setting_avatar_dialog_message), true); - } - - /** - * Result variable is a result of an or operation of all possible problems. Ie. if result - * is 0001 means IMAGE_DARK - * if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT - */ - public static String getErrorMessageForResult(Context context, @Result int result) { - StringBuilder errorMessage = new StringBuilder(); - if (result <= 0 ) { - Timber.d("No issues to warn user is found"); - } else { - Timber.d("Issues found to warn user"); - - errorMessage.append(context.getResources().getString(R.string.upload_problem_exist)); - - if ((IMAGE_DARK & result) != 0 ) { // We are checking image dark bit to see if that bit is set or not - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_dark)); - } - - if ((IMAGE_BLURRY & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_blurry)); - } - - if ((IMAGE_DUPLICATE & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_image_duplicate)); - } - - if ((IMAGE_GEOLOCATION_DIFFERENT & result) != 0 ) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_different_geolocation)); - } - - if ((FILE_FBMD & result) != 0) { - errorMessage.append("\n - ").append(context.getResources().getString(R.string.upload_problem_fbmd)); - } - - if ((FILE_NO_EXIF & result) != 0){ - errorMessage.append("\n - ").append(context.getResources().getString(R.string.internet_downloaded)); - } - - errorMessage.append("\n\n").append(context.getResources().getString(R.string.upload_problem_do_you_continue)); - } - - return errorMessage.toString(); - } - - /** - * Adds red border to a bitmap - * @param bitmap - * @param borderSize - * @param context - * @return - */ - public static Bitmap addRedBorder(Bitmap bitmap, int borderSize, Context context) { - Bitmap bmpWithBorder = Bitmap.createBitmap(bitmap.getWidth() + borderSize * 2, bitmap.getHeight() + borderSize * 2, bitmap.getConfig()); - Canvas canvas = new Canvas(bmpWithBorder); - canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)); - canvas.drawBitmap(bitmap, borderSize, borderSize, null); - return bmpWithBorder; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt new file mode 100644 index 0000000000..78a877600b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.kt @@ -0,0 +1,363 @@ +package fr.free.nrw.commons.utils + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.ProgressDialog +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.net.Uri +import android.os.Build +import androidx.annotation.IntDef +import androidx.core.content.ContextCompat +import androidx.exifinterface.media.ExifInterface +import androidx.work.Data +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.SetWallpaperWorker +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +/** + * Created by blueSir9 on 3/10/17. + */ + + +object ImageUtils { + + /** + * Set 0th bit as 1 for dark image ie. 0001 + */ + const val IMAGE_DARK = 1 shl 0 // 1 + + /** + * Set 1st bit as 1 for blurry image ie. 0010 + */ + const val IMAGE_BLURRY = 1 shl 1 // 2 + + /** + * Set 2nd bit as 1 for duplicate image ie. 0100 + */ + const val IMAGE_DUPLICATE = 1 shl 2 // 4 + + /** + * Set 3rd bit as 1 for image with different geo location ie. 1000 + */ + const val IMAGE_GEOLOCATION_DIFFERENT = 1 shl 3 // 8 + + /** + * The parameter FILE_FBMD is returned from the class ReadFBMD if the uploaded image contains + * FBMD data else returns IMAGE_OK + * ie. 10000 + */ + const val FILE_FBMD = 1 shl 4 // 16 + + /** + * The parameter FILE_NO_EXIF is returned from the class EXIFReader if the uploaded image does + * not contains EXIF data else returns IMAGE_OK + * ie. 100000 + */ + const val FILE_NO_EXIF = 1 shl 5 // 32 + + const val IMAGE_OK = 0 + const val IMAGE_KEEP = -1 + const val IMAGE_WAIT = -2 + const val EMPTY_CAPTION = -3 + const val FILE_NAME_EXISTS = 1 shl 6 // 64 + const val NO_CATEGORY_SELECTED = -5 + + private var progressDialogWallpaper: ProgressDialog? = null + + private var progressDialogAvatar: ProgressDialog? = null + + @IntDef( + flag = true, + value = [ + IMAGE_DARK, + IMAGE_BLURRY, + IMAGE_DUPLICATE, + IMAGE_OK, + IMAGE_KEEP, + IMAGE_WAIT, + EMPTY_CAPTION, + FILE_NAME_EXISTS, + NO_CATEGORY_SELECTED, + IMAGE_GEOLOCATION_DIFFERENT + ] + ) + @Retention + annotation class Result + + /** + * @return IMAGE_OK if image is not too dark + * IMAGE_DARK if image is too dark + */ + @JvmStatic + fun checkIfImageIsTooDark(imagePath: String): Int { + val millis = System.currentTimeMillis() + return try { + var bmp = ExifInterface(imagePath).thumbnailBitmap + if (bmp == null) { + bmp = BitmapFactory.decodeFile(imagePath) + } + + if (checkIfImageIsDark(bmp)) { + IMAGE_DARK + } else { + IMAGE_OK + } + } catch (e: Exception) { + Timber.d(e, "Error while checking image darkness.") + IMAGE_OK + } finally { + Timber.d("Checking image darkness took ${System.currentTimeMillis() - millis} ms.") + } + } + + /** + * @param geolocationOfFileString Geolocation of image. If geotag doesn't exists, then this will + * be an empty string + * @param latLng Location of wikidata item will be edited after upload + * @return false if image is neither dark nor blurry or if the input bitmapRegionDecoder provide + * d is null true if geolocation of the image and wikidata item are different + */ + @JvmStatic + fun checkImageGeolocationIsDifferent(geolocationOfFileString: String, latLng: LatLng?): Boolean { + Timber.d("Comparing geolocation of file with nearby place location") + if (latLng == null) { // Means that geolocation for this image is not given + return false // Since we don't know geolocation of file, we choose letting upload + } + + val geolocationOfFile = geolocationOfFileString.split("|") + val distance = LengthUtils.computeDistanceBetween( + LatLng(geolocationOfFile[0].toDouble(), geolocationOfFile[1].toDouble(), 0.0F), + latLng + ) + // Distance is more than 1 km, means that geolocation is wrong + return distance >= 1000 + } + + @JvmStatic + private fun checkIfImageIsDark(bitmap: Bitmap?): Boolean { + if (bitmap == null) { + Timber.e("Expected bitmap was null") + return true + } + + val bitmapWidth = bitmap.width + val bitmapHeight = bitmap.height + + val allPixelsCount = bitmapWidth * bitmapHeight + var numberOfBrightPixels = 0 + var numberOfMediumBrightnessPixels = 0 + val brightPixelThreshold = 0.025 * allPixelsCount + val mediumBrightPixelThreshold = 0.3 * allPixelsCount + + for (x in 0 until bitmapWidth) { + for (y in 0 until bitmapHeight) { + val pixel = bitmap.getPixel(x, y) + val r = Color.red(pixel) + val g = Color.green(pixel) + val b = Color.blue(pixel) + + val max = maxOf(r, g, b) / 255.0 + val min = minOf(r, g, b) / 255.0 + + val luminance = ((max + min) / 2.0) * 100 + + val highBrightnessLuminance = 40 + val mediumBrightnessLuminance = 26 + + if (luminance < highBrightnessLuminance) { + if (luminance > mediumBrightnessLuminance) { + numberOfMediumBrightnessPixels++ + } + } else { + numberOfBrightPixels++ + } + + if (numberOfBrightPixels >= brightPixelThreshold || numberOfMediumBrightnessPixels >= mediumBrightPixelThreshold) { + return false + } + } + } + return true + } + + /** + * Downloads the image from the URL and sets it as the phone's wallpaper + * Fails silently if download or setting wallpaper fails. + * + * @param context context + * @param imageUrl Url of the image + */ + @JvmStatic + fun setWallpaperFromImageUrl(context: Context, imageUrl: Uri) { + enqueueSetWallpaperWork(context, imageUrl) + } + + @JvmStatic + private fun createNotificationChannel(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val name = "Wallpaper Setting" + val description = "Notifications for wallpaper setting progress" + val importance = NotificationManager.IMPORTANCE_DEFAULT + val channel = NotificationChannel("set_wallpaper_channel", name, importance).apply { + this.description = description + } + val notificationManager = context.getSystemService(NotificationManager::class.java) + notificationManager.createNotificationChannel(channel) + } + } + + /** + * Calls the set avatar api to set the image url as user's avatar + * @param context + * @param url + * @param username + * @param okHttpJsonApiClient + * @param compositeDisposable + */ + @JvmStatic + fun setAvatarFromImageUrl( + context: Context, + url: String, + username: String, + okHttpJsonApiClient: OkHttpJsonApiClient, + compositeDisposable: CompositeDisposable + ) { + showSettingAvatarProgressBar(context) + + try { + compositeDisposable.add( + okHttpJsonApiClient + .setAvatar(username, url) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response?.status == "200") { + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully)) + progressDialogAvatar?.dismiss() + } + }, + { t -> + Timber.e(t, "Setting Avatar Failed") + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)) + progressDialogAvatar?.cancel() + } + ) + ) + } catch (e: Exception) { + Timber.d("$e success") + ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully)) + progressDialogAvatar?.cancel() + } + } + + @JvmStatic + fun enqueueSetWallpaperWork(context: Context, imageUrl: Uri) { + createNotificationChannel(context) // Ensure the notification channel is created + + val inputData = Data.Builder() + .putString("imageUrl", imageUrl.toString()) + .build() + + val setWallpaperWork = OneTimeWorkRequest.Builder(SetWallpaperWorker::class.java) + .setInputData(inputData) + .build() + + WorkManager.getInstance(context).enqueue(setWallpaperWork) + } + + @JvmStatic + private fun showSettingWallpaperProgressBar(context: Context) { + progressDialogWallpaper = ProgressDialog.show( + context, + context.getString(R.string.setting_wallpaper_dialog_title), + context.getString(R.string.setting_wallpaper_dialog_message), + true + ) + } + + @JvmStatic + private fun showSettingAvatarProgressBar(context: Context) { + progressDialogAvatar = ProgressDialog.show( + context, + context.getString(R.string.setting_avatar_dialog_title), + context.getString(R.string.setting_avatar_dialog_message), + true + ) + } + + /** + * Adds red border to bitmap with specified border size + * * @param bitmap + * * @param borderSize + * * @param context + * * @return + */ + @JvmStatic + fun addRedBorder(bitmap: Bitmap, borderSize: Int, context: Context): Bitmap { + val bmpWithBorder = Bitmap.createBitmap( + bitmap.width + borderSize * 2, + bitmap.height + borderSize * 2, + bitmap.config + ) + val canvas = Canvas(bmpWithBorder) + canvas.drawColor(ContextCompat.getColor(context, R.color.deleteRed)) + canvas.drawBitmap(bitmap, borderSize.toFloat(), borderSize.toFloat(), null) + return bmpWithBorder + } + + /** + * Result variable is a result of an or operation of all possible problems. Ie. if result + * is 0001 means IMAGE_DARK + * if result is 1100 IMAGE_DUPLICATE and IMAGE_GEOLOCATION_DIFFERENT + */ + @JvmStatic + fun getErrorMessageForResult(context: Context, @Result result: Int): String { + val errorMessage = StringBuilder() + if (result <= 0) { + Timber.d("No issues to warn user are found") + } else { + Timber.d("Issues found to warn user") + errorMessage.append(context.getString(R.string.upload_problem_exist)) + + if (result and IMAGE_DARK != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_image_dark)) + } + if (result and IMAGE_BLURRY != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_image_blurry)) + } + if (result and IMAGE_DUPLICATE != 0) { + errorMessage.append("\n - "). + append(context.getString(R.string.upload_problem_image_duplicate)) + } + if (result and IMAGE_GEOLOCATION_DIFFERENT != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_different_geolocation)) + } + if (result and FILE_FBMD != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.upload_problem_fbmd)) + } + if (result and FILE_NO_EXIF != 0) { + errorMessage.append("\n - ") + .append(context.getString(R.string.internet_downloaded)) + } + errorMessage.append("\n\n") + .append(context.getString(R.string.upload_problem_do_you_continue)) + } + return errorMessage.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java deleted file mode 100644 index 634a73ad23..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import io.reactivex.Single; -import io.reactivex.schedulers.Schedulers; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ImageUtilsWrapper { - - @Inject - public ImageUtilsWrapper() { - - } - - public Single checkIfImageIsTooDark(String bitmapPath) { - return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath)) - .subscribeOn(Schedulers.computation()); - } - - public Single checkImageGeolocationIsDifferent(String geolocationOfFileString, - LatLng latLng) { - return Single.fromCallable( - () -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng)) - .subscribeOn(Schedulers.computation()) - .map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT - : ImageUtils.IMAGE_OK); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt new file mode 100644 index 0000000000..2e0efc6901 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ImageUtilsWrapper @Inject constructor() { + + fun checkIfImageIsTooDark(bitmapPath: String): Single { + return Single.fromCallable { ImageUtils.checkIfImageIsTooDark(bitmapPath) } + .subscribeOn(Schedulers.computation()) + } + + fun checkImageGeolocationIsDifferent( + geolocationOfFileString: String, + latLng: LatLng + ): Single { + return Single.fromCallable { + ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng) + } + .subscribeOn(Schedulers.computation()) + .map { isDifferent -> + if (isDifferent) ImageUtils.IMAGE_GEOLOCATION_DIFFERENT else ImageUtils.IMAGE_OK + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java deleted file mode 100644 index 73bd5c02b3..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.utils; -import android.content.Context; -import android.content.res.Configuration; -import android.content.res.Resources; -import java.util.Locale; - -/** - * Utilities class for miscellaneous strings - */ -public class LangCodeUtils { - /** - * Replaces the deprecated ISO-639 language codes used by Android with the updated ISO-639-1. - * @param code Language code you want to update. - * @return Updated language code. If not in the "deprecated list" returns the same code. - */ - public static String fixLanguageCode(String code) { - if (code.equalsIgnoreCase("iw")) { - return "he"; - } else if (code.equalsIgnoreCase("in")) { - return "id"; - } else if (code.equalsIgnoreCase("ji")) { - return "yi"; - } else { - return code; - } - } - - /** - * Returns configuration for locale of - * our choice regardless of user's device settings - */ - public static Resources getLocalizedResources(Context context, Locale desiredLocale) { - Configuration conf = context.getResources().getConfiguration(); - conf = new Configuration(conf); - conf.setLocale(desiredLocale); - Context localizedContext = context.createConfigurationContext(conf); - return localizedContext.getResources(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt new file mode 100644 index 0000000000..5ef21a735b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LangCodeUtils.kt @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.res.Configuration +import android.content.res.Resources +import java.util.Locale + +/** + * Utilities class for miscellaneous strings + */ +object LangCodeUtils { + + /** + * Replaces the deprecated ISO-639 language codes used by Android with the updated ISO-639-1. + * @param code Language code you want to update. + * @return Updated language code. If not in the "deprecated list" returns the same code. + */ + @JvmStatic + fun fixLanguageCode(code: String): String { + return when (code.lowercase()) { + "iw" -> "he" + "in" -> "id" + "ji" -> "yi" + else -> code + } + } + + /** + * Returns configuration for locale of + * our choice regardless of user's device settings + */ + @JvmStatic + fun getLocalizedResources(context: Context, desiredLocale: Locale): Resources { + val conf = Configuration(context.resources.configuration).apply { + setLocale(desiredLocale) + } + val localizedContext = context.createConfigurationContext(conf) + return localizedContext.resources + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java deleted file mode 100644 index 76c52527b1..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Context; -import android.util.DisplayMetrics; -import android.view.View; -import android.view.ViewGroup; -import android.view.ViewTreeObserver; - -public class LayoutUtils { - - /** - * Can be used for keeping aspect radios suggested by material guidelines. See: - * https://material.io/design/layout/spacing-methods.html#containers-aspect-ratios - * In some cases we don't know exact width, for such cases this method measures - * width and sets height by multiplying the width with height. - * @param rate Aspect ratios, ie 1 for 1:1. (width * rate = height) - * @param view view to change height - */ - public static void setLayoutHeightAllignedToWidth(double rate, View view) { - ViewTreeObserver vto = view.getViewTreeObserver(); - vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - view.getViewTreeObserver().removeOnGlobalLayoutListener(this); - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - layoutParams.height = (int) (view.getWidth() * rate); - view.setLayoutParams(layoutParams); - } - }); - } - - public static double getScreenWidth(Context context, double rate) { - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity)context).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - return displayMetrics.widthPixels * rate; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt new file mode 100644 index 0000000000..71e6697f77 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LayoutUtils.kt @@ -0,0 +1,47 @@ +package fr.free.nrw.commons.utils + +import android.app.Activity +import android.content.Context +import android.util.DisplayMetrics +import android.view.View +import android.view.ViewTreeObserver + +/** + * Utility class for layout-related operations. + */ +object LayoutUtils { + + /** + * Can be used for keeping aspect ratios suggested by material guidelines. See: + * https://material.io/design/layout/spacing-methods.html#containers-aspect-ratios + * In some cases, we don't know the exact width, for such cases this method measures + * width and sets height by multiplying the width with height. + * @param rate Aspect ratios, i.e., 1 for 1:1 (width * rate = height) + * @param view View to change height + */ + @JvmStatic + fun setLayoutHeightAlignedToWidth(rate: Double, view: View) { + val vto = view.viewTreeObserver + vto.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + view.viewTreeObserver.removeOnGlobalLayoutListener(this) + val layoutParams = view.layoutParams + layoutParams.height = (view.width * rate).toInt() + view.layoutParams = layoutParams + } + }) + } + + /** + * Calculates and returns the screen width multiplied by the provided rate. + * @param context Context used to access display metrics. + * @param rate Multiplier for screen width. + * @return Calculated screen width multiplied by the rate. + */ + @JvmStatic + fun getScreenWidth(context: Context, rate: Double): Double { + val displayMetrics = DisplayMetrics() + (context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics) + return displayMetrics.widthPixels * rate + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java deleted file mode 100644 index 0ca61a1d9c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.java +++ /dev/null @@ -1,145 +0,0 @@ -package fr.free.nrw.commons.utils; - -import androidx.annotation.NonNull; - -import java.text.NumberFormat; - -import fr.free.nrw.commons.location.LatLng; - -public class LengthUtils { - /** - * Returns a formatted distance string between two points. - * - * @param point1 LatLng type point1 - * @param point2 LatLng type point2 - * @return string distance - */ - public static String formatDistanceBetween(LatLng point1, LatLng point2) { - if (point1 == null || point2 == null) { - return null; - } - - int distance = (int) Math.round(computeDistanceBetween(point1, point2)); - return formatDistance(distance); - } - - /** - * Format a distance (in meters) as a string - * Example: 140 -> "140m" - * 3841 -> "3.8km" - * - * @param distance Distance, in meters - * @return A string representing the distance - * @throws IllegalArgumentException If distance is negative - */ - public static String formatDistance(int distance) { - if (distance < 0) { - throw new IllegalArgumentException("Distance must be non-negative"); - } - - NumberFormat numberFormat = NumberFormat.getNumberInstance(); - - // Adjust to km if distance is over 1000m (1km) - if (distance >= 1000) { - numberFormat.setMaximumFractionDigits(1); - return numberFormat.format(distance / 1000.0) + "km"; - } - - // Otherwise just return in meters - return numberFormat.format(distance) + "m"; - } - - /** - * Computes the distance between two points. - * - * @param point1 LatLng type point1 - * @param point2 LatLng type point2 - * @return distance between the points in meters - * @throws NullPointerException if one or both the points are null - */ - public static double computeDistanceBetween(@NonNull LatLng point1, @NonNull LatLng point2) { - return computeAngleBetween(point1, point2) * 6371009.0D; // Earth's radius in meter - } - - /** - * Computes angle between two points - * - * @param point1 one of the two end points - * @param point2 one of the two end points - * @return Angle in radius - * @throws NullPointerException if one or both the points are null - */ - private static double computeAngleBetween(@NonNull LatLng point1, @NonNull LatLng point2) { - return distanceRadians( - Math.toRadians(point1.getLatitude()), - Math.toRadians(point1.getLongitude()), - Math.toRadians(point2.getLatitude()), - Math.toRadians(point2.getLongitude()) - ); - } - - /** - * Computes arc length between 2 points - * - * @param lat1 Latitude of point A - * @param lng1 Longitude of point A - * @param lat2 Latitude of point B - * @param lng2 Longitude of point B - * @return Arc length between the points - */ - private static double distanceRadians(double lat1, double lng1, double lat2, double lng2) { - return arcHav(havDistance(lat1, lat2, lng1 - lng2)); - } - - /** - * Computes inverse of haversine - * - * @param x Angle in radian - * @return Inverse of haversine - */ - private static double arcHav(double x) { - return 2.0D * Math.asin(Math.sqrt(x)); - } - - /** - * Computes distance between two points that are on same Longitude - * - * @param lat1 Latitude of point A - * @param lat2 Latitude of point B - * @param longitude Longitude on which they lie - * @return Arc length between points - */ - private static double havDistance(double lat1, double lat2, double longitude) { - return hav(lat1 - lat2) + hav(longitude) * Math.cos(lat1) * Math.cos(lat2); - } - - /** - * Computes haversine - * - * @param x Angle in radians - * @return Haversine of x - */ - private static double hav(double x) { - double sinHalf = Math.sin(x * 0.5D); - return sinHalf * sinHalf; - } - - /** - * Computes bearing between the two given points - * - * @see Bearing - * @param point1 Coordinates of first point - * @param point2 Coordinates of second point - * @return Bearing between the two end points in degrees - * @throws NullPointerException if one or both the points are null - */ - public static double computeBearing(@NonNull LatLng point1, @NonNull LatLng point2) { - double diffLongitute = Math.toRadians(point2.getLongitude() - point1.getLongitude()); - double lat1 = Math.toRadians(point1.getLatitude()); - double lat2 = Math.toRadians(point2.getLatitude()); - double y = Math.sin(diffLongitute) * Math.cos(lat2); - double x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(diffLongitute); - double bearing = Math.atan2(y, x); - return (Math.toDegrees(bearing) + 360) % 360; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt new file mode 100644 index 0000000000..48cf1a0209 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LengthUtils.kt @@ -0,0 +1,156 @@ +package fr.free.nrw.commons.utils + +import java.text.NumberFormat +import fr.free.nrw.commons.location.LatLng +import kotlin.math.asin +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.roundToInt +import kotlin.math.sin +import kotlin.math.sqrt + +object LengthUtils { + /** + * Returns a formatted distance string between two points. + * + * @param point1 LatLng type point1 + * @param point2 LatLng type point2 + * @return string distance + */ + @JvmStatic + fun formatDistanceBetween(point1: LatLng?, point2: LatLng?): String? { + if (point1 == null || point2 == null) { + return null + } + + val distance = computeDistanceBetween(point1, point2).roundToInt() + return formatDistance(distance) + } + + /** + * Format a distance (in meters) as a string + * Example: 140 -> "140m" + * 3841 -> "3.8km" + * + * @param distance Distance, in meters + * @return A string representing the distance + * @throws IllegalArgumentException If distance is negative + */ + @JvmStatic + fun formatDistance(distance: Int): String { + if (distance < 0) { + throw IllegalArgumentException("Distance must be non-negative") + } + + val numberFormat = NumberFormat.getNumberInstance() + + // Adjust to km if distance is over 1000m (1km) + return if (distance >= 1000) { + numberFormat.maximumFractionDigits = 1 + "${numberFormat.format(distance / 1000.0)}km" + } else { + "${numberFormat.format(distance)}m" + } + } + + /** + * Computes the distance between two points. + * + * @param point1 LatLng type point1 + * @param point2 LatLng type point2 + * @return distance between the points in meters + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + fun computeDistanceBetween(point1: LatLng, point2: LatLng): Double { + return computeAngleBetween(point1, point2) * 6371009.0 // Earth's radius in meters + } + + /** + * Computes angle between two points + * + * @param point1 one of the two end points + * @param point2 one of the two end points + * @return Angle in radians + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + private fun computeAngleBetween(point1: LatLng, point2: LatLng): Double { + return distanceRadians( + Math.toRadians(point1.latitude), + Math.toRadians(point1.longitude), + Math.toRadians(point2.latitude), + Math.toRadians(point2.longitude) + ) + } + + /** + * Computes arc length between 2 points + * + * @param lat1 Latitude of point A + * @param lng1 Longitude of point A + * @param lat2 Latitude of point B + * @param lng2 Longitude of point B + * @return Arc length between the points + */ + @JvmStatic + private fun distanceRadians(lat1: Double, lng1: Double, lat2: Double, lng2: Double): Double { + return arcHav(havDistance(lat1, lat2, lng1 - lng2)) + } + + /** + * Computes inverse of haversine + * + * @param x Angle in radian + * @return Inverse of haversine + */ + @JvmStatic + private fun arcHav(x: Double): Double { + return 2.0 * asin(sqrt(x)) + } + + /** + * Computes distance between two points that are on same Longitude + * + * @param lat1 Latitude of point A + * @param lat2 Latitude of point B + * @param longitude Longitude on which they lie + * @return Arc length between points + */ + @JvmStatic + private fun havDistance(lat1: Double, lat2: Double, longitude: Double): Double { + return hav(lat1 - lat2) + hav(longitude) * cos(lat1) * cos(lat2) + } + + /** + * Computes haversine + * + * @param x Angle in radians + * @return Haversine of x + */ + @JvmStatic + private fun hav(x: Double): Double { + val sinHalf = sin(x * 0.5) + return sinHalf * sinHalf + } + + /** + * Computes bearing between the two given points + * + * @see Bearing + * @param point1 Coordinates of first point + * @param point2 Coordinates of second point + * @return Bearing between the two end points in degrees + * @throws NullPointerException if one or both the points are null + */ + @JvmStatic + fun computeBearing(point1: LatLng, point2: LatLng): Double { + val diffLongitude = Math.toRadians(point2.longitude - point1.longitude) + val lat1 = Math.toRadians(point1.latitude) + val lat2 = Math.toRadians(point2.latitude) + val y = sin(diffLongitude) * cos(lat2) + val x = cos(lat1) * sin(lat2) - sin(lat1) * cos(lat2) * cos(diffLongitude) + val bearing = atan2(y, x) + return (Math.toDegrees(bearing) + 360) % 360 + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java deleted file mode 100644 index 01a8855387..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.java +++ /dev/null @@ -1,58 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import timber.log.Timber; - -public class LocationUtils { - public static final double RADIUS_OF_EARTH_KM = 6371.0; // Earth's radius in kilometers - - public static LatLng deriveUpdatedLocationFromSearchQuery(String customQuery) { - LatLng latLng = null; - final int indexOfPrefix = customQuery.indexOf("Point("); - if (indexOfPrefix == -1) { - Timber.e("Invalid prefix index - Seems like user has entered an invalid query"); - return latLng; - } - final int indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix); - if (indexOfSuffix == -1) { - Timber.e("Invalid suffix index - Seems like user has entered an invalid query"); - return latLng; - } - String latLngString = customQuery.substring(indexOfPrefix+"Point(".length(), indexOfSuffix); - if (latLngString.isEmpty()) { - return null; - } - - String latLngArray[] = latLngString.split(" "); - if (latLngArray.length != 2) { - return null; - } - - try { - latLng = new LatLng(Double.parseDouble(latLngArray[1].trim()), - Double.parseDouble(latLngArray[0].trim()), 1f); - }catch (Exception e){ - Timber.e("Error while parsing user entered lat long: %s", e); - } - - return latLng; - } - - - public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) { - double lat1Rad = Math.toRadians(lat1); - double lon1Rad = Math.toRadians(lon1); - double lat2Rad = Math.toRadians(lat2); - double lon2Rad = Math.toRadians(lon2); - - // Haversine formula - double dlon = lon2Rad - lon1Rad; - double dlat = lat2Rad - lat1Rad; - double a = Math.pow(Math.sin(dlat / 2), 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.pow(Math.sin(dlon / 2), 2); - double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - double distance = RADIUS_OF_EARTH_KM * c; - - return distance; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt new file mode 100644 index 0000000000..2df42270eb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/LocationUtils.kt @@ -0,0 +1,63 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import timber.log.Timber +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.sqrt + +object LocationUtils { + const val RADIUS_OF_EARTH_KM = 6371.0 // Earth's radius in kilometers + + @JvmStatic + fun deriveUpdatedLocationFromSearchQuery(customQuery: String): LatLng? { + var latLng: LatLng? = null + val indexOfPrefix = customQuery.indexOf("Point(") + if (indexOfPrefix == -1) { + Timber.e("Invalid prefix index - Seems like user has entered an invalid query") + return latLng + } + val indexOfSuffix = customQuery.indexOf(")\"", indexOfPrefix) + if (indexOfSuffix == -1) { + Timber.e("Invalid suffix index - Seems like user has entered an invalid query") + return latLng + } + val latLngString = customQuery.substring(indexOfPrefix + "Point(".length, indexOfSuffix) + if (latLngString.isEmpty()) { + return null + } + + val latLngArray = latLngString.split(" ") + if (latLngArray.size != 2) { + return null + } + + try { + latLng = LatLng(latLngArray[1].trim().toDouble(), + latLngArray[0].trim().toDouble(), 1f) + } catch (e: Exception) { + Timber.e("Error while parsing user entered lat long: %s", e) + } + + return latLng + } + + @JvmStatic + fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { + val lat1Rad = Math.toRadians(lat1) + val lon1Rad = Math.toRadians(lon1) + val lat2Rad = Math.toRadians(lat2) + val lon2Rad = Math.toRadians(lon2) + + // Haversine formula + val dlon = lon2Rad - lon1Rad + val dlat = lat2Rad - lat1Rad + val a = Math.pow( + sin(dlat / 2), 2.0) + cos(lat1Rad) * cos(lat2Rad) * Math.pow(sin(dlon / 2), 2.0 + ) + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return RADIUS_OF_EARTH_KM * c + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java deleted file mode 100644 index d3b5bd0e24..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.location.LocationUpdateListener; -import timber.log.Timber; - -public class MapUtils { - public static final float ZOOM_LEVEL = 14f; - public static final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005; - public static final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004; - public static final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE"; - public static final float ZOOM_OUT = 0f; - - public static final LatLng defaultLatLng = new fr.free.nrw.commons.location.LatLng(51.50550,-0.07520,1f); - - public static void registerUnregisterLocationListener(final boolean removeLocationListener, LocationServiceManager locationManager, LocationUpdateListener locationUpdateListener) { - try { - if (removeLocationListener) { - locationManager.unregisterLocationManager(); - locationManager.removeLocationListener(locationUpdateListener); - Timber.d("Location service manager unregistered and removed"); - } else { - locationManager.addLocationListener(locationUpdateListener); - locationManager.registerLocationManager(); - Timber.d("Location service manager added and registered"); - } - }catch (final Exception e){ - Timber.e(e); - //Broadcasts are tricky, should be catchedonR - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt new file mode 100644 index 0000000000..adc3a5d908 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MapUtils.kt @@ -0,0 +1,39 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.location.LocationUpdateListener +import timber.log.Timber + +object MapUtils { + const val ZOOM_LEVEL = 14f + const val CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.005 + const val CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.004 + const val NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE" + const val ZOOM_OUT = 0f + + @JvmStatic + val defaultLatLng = LatLng(51.50550, -0.07520, 1f) + + @JvmStatic + fun registerUnregisterLocationListener( + removeLocationListener: Boolean, + locationManager: LocationServiceManager, + locationUpdateListener: LocationUpdateListener + ) { + try { + if (removeLocationListener) { + locationManager.unregisterLocationManager() + locationManager.removeLocationListener(locationUpdateListener) + Timber.d("Location service manager unregistered and removed") + } else { + locationManager.addLocationListener(locationUpdateListener) + locationManager.registerLocationManager() + Timber.d("Location service manager added and registered") + } + } catch (e: Exception) { + Timber.e(e) + // Broadcasts are tricky, should be caught on onR + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java deleted file mode 100644 index 8eb875bb56..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.java +++ /dev/null @@ -1,29 +0,0 @@ -package fr.free.nrw.commons.utils; - -import org.apache.commons.lang3.StringUtils; - -import java.util.ArrayList; -import java.util.List; - -public class MediaDataExtractorUtil { - /** - * Extracts a list of categories from | separated category string - * - * @param source - * @return - */ - public static List extractCategoriesFromList(String source) { - if (StringUtils.isBlank(source)) { - return new ArrayList<>(); - } - String[] cats = source.split("\\|"); - List categories = new ArrayList<>(); - for (String category : cats) { - if (!StringUtils.isBlank(category.trim())) { - categories.add(category); - } - } - return categories; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt new file mode 100644 index 0000000000..9e46525da5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.utils + +import org.apache.commons.lang3.StringUtils + +import java.util.ArrayList + +object MediaDataExtractorUtil { + + /** + * Extracts a list of categories from | separated category string + * + * @param source + * @return + */ + @JvmStatic + fun extractCategoriesFromList(source: String): List { + if (source.isBlank()) { + return emptyList() + } + val cats = source.split("|") + val categories = mutableListOf() + for (category in cats) { + if (category.trim().isNotBlank()) { + categories.add(category) + } + } + return categories + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java deleted file mode 100644 index bc6e6883ff..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.java +++ /dev/null @@ -1,51 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; - -import androidx.coordinatorlayout.widget.CoordinatorLayout; - -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -public class NearbyFABUtils { - /* - * Add anchors back before making them visible again. - * */ - public static void addAnchorToBigFABs(FloatingActionButton floatingActionButton, int anchorID) { - CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams - (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); - params.setAnchorId(anchorID); - params.anchorGravity = Gravity.TOP|Gravity.RIGHT|Gravity.END; - floatingActionButton.setLayoutParams(params); - } - - /* - * Add anchors back before making them visible again. Big and small fabs have different anchor - * gravities, therefore the are two methods. - * */ - public static void addAnchorToSmallFABs(FloatingActionButton floatingActionButton, int anchorID) { - CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams - (ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT); - params.setAnchorId(anchorID); - params.anchorGravity = Gravity.CENTER_HORIZONTAL; - floatingActionButton.setLayoutParams(params); - } - - /* - * We are not able to hide FABs without removing anchors, this method removes anchors - * */ - public static void removeAnchorFromFAB(FloatingActionButton floatingActionButton) { - //get rid of anchors - //Somehow this was the only way https://stackoverflow.com/questions/32732932 - // /floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone - CoordinatorLayout.LayoutParams param = (CoordinatorLayout.LayoutParams) floatingActionButton - .getLayoutParams(); - param.setAnchorId(View.NO_ID); - // If we don't set them to zero, then they become visible for a moment on upper left side - param.width = 0; - param.height = 0; - floatingActionButton.setLayoutParams(param); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt new file mode 100644 index 0000000000..61b95a4139 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NearbyFABUtils.kt @@ -0,0 +1,55 @@ +package fr.free.nrw.commons.utils + +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.floatingactionbutton.FloatingActionButton + +object NearbyFABUtils { + + /* + * Add anchors back before making them visible again. + */ + @JvmStatic + fun addAnchorToBigFABs(floatingActionButton: FloatingActionButton, anchorID: Int) { + val params = CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.anchorId = anchorID + params.anchorGravity = Gravity.TOP or Gravity.RIGHT or Gravity.END + floatingActionButton.layoutParams = params + } + + /* + * Add anchors back before making them visible again. Big and small fabs have different anchor + * gravities, therefore there are two methods. + */ + @JvmStatic + fun addAnchorToSmallFABs(floatingActionButton: FloatingActionButton, anchorID: Int) { + val params = CoordinatorLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + params.anchorId = anchorID + params.anchorGravity = Gravity.CENTER_HORIZONTAL + floatingActionButton.layoutParams = params + } + + /* + * We are not able to hide FABs without removing anchors, this method removes anchors. + */ + @JvmStatic + fun removeAnchorFromFAB(floatingActionButton: FloatingActionButton) { + // get rid of anchors + // Somehow this was the only way https://stackoverflow.com/questions/32732932 + // floatingactionbutton-visible-for-sometime-even-if-visibility-is-set-to-gone + val params = floatingActionButton.layoutParams as CoordinatorLayout.LayoutParams + params.anchorId = View.NO_ID + // If we don't set them to zero, then they become visible for a moment on upper left side + params.width = 0 + params.height = 0 + floatingActionButton.layoutParams = params + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java deleted file mode 100644 index ce64cb0317..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.utils; - - -import android.annotation.SuppressLint; -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.NetworkInfo; -import android.telephony.TelephonyManager; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.utils.model.NetworkConnectionType; - -public class NetworkUtils { - - /** - * https://developer.android.com/training/monitoring-device-state/connectivity-monitoring#java - * Check if internet connection is established. - * - * @param context context passed to this method could be null. - * @return Returns current internet connection status. Returns false if null context was passed. - */ - @SuppressLint("MissingPermission") - public static boolean isInternetConnectionEstablished(@Nullable Context context) { - if (context == null) { - return false; - } - - NetworkInfo activeNetwork = getNetworkInfo(context); - return activeNetwork != null && activeNetwork.isConnectedOrConnecting(); - } - - /** - * Detect network connection type - */ - static NetworkConnectionType getNetworkType(Context context) { - TelephonyManager telephonyManager = (TelephonyManager) context.getApplicationContext().getSystemService(Context.TELEPHONY_SERVICE); - if (telephonyManager == null) { - return NetworkConnectionType.UNKNOWN; - } - - NetworkInfo networkInfo = getNetworkInfo(context); - if (networkInfo == null) { - return NetworkConnectionType.UNKNOWN; - } - - int network = networkInfo.getType(); - if (network == ConnectivityManager.TYPE_WIFI) { - return NetworkConnectionType.WIFI; - } - - // TODO for Android 12+ request permission from user is mandatory - /* - int mobileNetwork = telephonyManager.getNetworkType(); - switch (mobileNetwork) { - case TelephonyManager.NETWORK_TYPE_GPRS: - case TelephonyManager.NETWORK_TYPE_EDGE: - case TelephonyManager.NETWORK_TYPE_CDMA: - case TelephonyManager.NETWORK_TYPE_1xRTT: - return NetworkConnectionType.TWO_G; - case TelephonyManager.NETWORK_TYPE_HSDPA: - case TelephonyManager.NETWORK_TYPE_UMTS: - case TelephonyManager.NETWORK_TYPE_HSUPA: - case TelephonyManager.NETWORK_TYPE_HSPA: - case TelephonyManager.NETWORK_TYPE_EHRPD: - case TelephonyManager.NETWORK_TYPE_EVDO_0: - case TelephonyManager.NETWORK_TYPE_EVDO_A: - case TelephonyManager.NETWORK_TYPE_EVDO_B: - return NetworkConnectionType.THREE_G; - case TelephonyManager.NETWORK_TYPE_LTE: - case TelephonyManager.NETWORK_TYPE_HSPAP: - return NetworkConnectionType.FOUR_G; - default: - return NetworkConnectionType.UNKNOWN; - } - */ - return NetworkConnectionType.UNKNOWN; - } - - /** - * Extracted private method to get nullable network info - */ - @Nullable - private static NetworkInfo getNetworkInfo(Context context) { - ConnectivityManager connectivityManager = - (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE); - - if (connectivityManager == null) { - return null; - } - - return connectivityManager.getActiveNetworkInfo(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt new file mode 100644 index 0000000000..98fde9ef70 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/NetworkUtils.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.net.ConnectivityManager +import android.net.NetworkInfo +import android.telephony.TelephonyManager + +import fr.free.nrw.commons.utils.model.NetworkConnectionType + +object NetworkUtils { + + /** + * https://developer.android.com/training/monitoring-device-state/connectivity-monitoring#java + * Check if internet connection is established. + * + * @param context context passed to this method could be null. + * @return Returns current internet connection status. Returns false if null context was passed. + */ + @SuppressLint("MissingPermission") + @JvmStatic + fun isInternetConnectionEstablished(context: Context?): Boolean { + if (context == null) { + return false + } + + val activeNetwork = getNetworkInfo(context) + return activeNetwork != null && activeNetwork.isConnectedOrConnecting + } + + /** + * Detect network connection type + */ + @JvmStatic + fun getNetworkType(context: Context): NetworkConnectionType { + val telephonyManager = context.applicationContext.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager + ?: return NetworkConnectionType.UNKNOWN + + val networkInfo = getNetworkInfo(context) + ?: return NetworkConnectionType.UNKNOWN + + val network = networkInfo.type + if (network == ConnectivityManager.TYPE_WIFI) { + return NetworkConnectionType.WIFI + } + + // TODO for Android 12+ request permission from user is mandatory + /* + val mobileNetwork = telephonyManager.networkType + return when (mobileNetwork) { + TelephonyManager.NETWORK_TYPE_GPRS, + TelephonyManager.NETWORK_TYPE_EDGE, + TelephonyManager.NETWORK_TYPE_CDMA, + TelephonyManager.NETWORK_TYPE_1xRTT -> NetworkConnectionType.TWO_G + + TelephonyManager.NETWORK_TYPE_HSDPA, + TelephonyManager.NETWORK_TYPE_UMTS, + TelephonyManager.NETWORK_TYPE_HSUPA, + TelephonyManager.NETWORK_TYPE_HSPA, + TelephonyManager.NETWORK_TYPE_EHRPD, + TelephonyManager.NETWORK_TYPE_EVDO_0, + TelephonyManager.NETWORK_TYPE_EVDO_A, + TelephonyManager.NETWORK_TYPE_EVDO_B -> NetworkConnectionType.THREE_G + + TelephonyManager.NETWORK_TYPE_LTE, + TelephonyManager.NETWORK_TYPE_HSPAP -> NetworkConnectionType.FOUR_G + + else -> NetworkConnectionType.UNKNOWN + } + */ + return NetworkConnectionType.UNKNOWN + } + + /** + * Extracted private method to get nullable network info + */ + @JvmStatic + private fun getNetworkInfo(context: Context): NetworkInfo? { + val connectivityManager = + context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager + ?: return null + + return connectivityManager.activeNetworkInfo + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java deleted file mode 100644 index 692194234f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ /dev/null @@ -1,224 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.Manifest; -import android.Manifest.permission; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.os.Build; -import android.provider.Settings; -import android.widget.Toast; -import androidx.annotation.StringRes; -import androidx.core.content.ContextCompat; -import com.karumi.dexter.Dexter; -import com.karumi.dexter.MultiplePermissionsReport; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.multi.MultiplePermissionsListener; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.upload.UploadActivity; -import java.util.List; - -public class PermissionUtils { - public static String[] PERMISSIONS_STORAGE = getPermissionsStorage(); - - static String[] getPermissionsStorage() { - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return new String[]{ Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU) { - return new String[]{ Manifest.permission.READ_MEDIA_IMAGES, - Manifest. permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { - return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) { - return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_MEDIA_LOCATION }; - } - return new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE }; - } - - /** - * This method can be used by any activity which requires a permission which has been - * blocked(marked never ask again by the user) It open the app settings from where the user can - * manually give us the required permission. - * - * @param activity The Activity which requires a permission which has been blocked - */ - private static void askUserToManuallyEnablePermissionFromSettings(final Activity activity) { - final Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - final Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - } - - /** - * Checks whether the app already has a particular permission - * - * @param activity The Activity context to check permissions against - * @param permissions An array of permission strings to check - * @return `true if the app has all the specified permissions, `false` otherwise - */ - public static boolean hasPermission(final Activity activity, final String[] permissions) { - boolean hasPermission = true; - for(final String permission : permissions) { - hasPermission = hasPermission && - ContextCompat.checkSelfPermission(activity, permission) - == PackageManager.PERMISSION_GRANTED; - } - return hasPermission; - } - - public static boolean hasPartialAccess(final Activity activity) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { - return ContextCompat.checkSelfPermission(activity, - permission.READ_MEDIA_VISUAL_USER_SELECTED - ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( - activity, permission.READ_MEDIA_IMAGES - ) == PackageManager.PERMISSION_DENIED; - } - return false; - } - - /** - * Checks for a particular permission and runs the runnable to perform an action when the - * permission is granted Also, it shows a rationale if needed - *

- * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no - * permission rationale will be displayed and permission would be requested - *

- * Sample usage: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - * R.string.storage_permission_title, R.string.write_storage_permission_rationale); - *

- * If you don't want the permission rationale to be shown then use: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - 1, -1); - * - * @param activity activity requesting permissions - * @param permissions the permissions array being requests - * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param rationaleTitle rationale title to be displayed when permission was denied. It can - * be an invalid @StringRes - * @param rationaleMessage rationale message to be displayed when permission was denied. It - * can be an invalid @StringRes - */ - public static void checkPermissionsAndPerformAction( - final Activity activity, - final Runnable onPermissionGranted, - final @StringRes int rationaleTitle, - final @StringRes int rationaleMessage, - final String... permissions - ) { - if (hasPartialAccess(activity)) { - onPermissionGranted.run(); - return; - } - checkPermissionsAndPerformAction(activity, onPermissionGranted, null, - rationaleTitle, rationaleMessage, permissions); - } - - /** - * Checks for a particular permission and runs the corresponding runnables to perform an action - * when the permission is granted/denied Also, it shows a rationale if needed - *

- * Sample usage: - *

- * PermissionUtils.checkPermissionsAndPerformAction(activity, - * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), () -> - * showMessage(), R.string.storage_permission_title, - * R.string.write_storage_permission_rationale); - * - * @param activity activity requesting permissions - * @param permissions the permissions array being requested - * @param onPermissionGranted the runnable to be executed when the permission is granted - * @param onPermissionDenied the runnable to be executed when the permission is denied(but not - * permanently) - * @param rationaleTitle rationale title to be displayed when permission was denied - * @param rationaleMessage rationale message to be displayed when permission was denied - */ - public static void checkPermissionsAndPerformAction( - final Activity activity, - final Runnable onPermissionGranted, - final Runnable onPermissionDenied, - final @StringRes int rationaleTitle, - final @StringRes int rationaleMessage, - final String... permissions - ) { - Dexter.withActivity(activity) - .withPermissions(permissions) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(final MultiplePermissionsReport report) { - if (report.areAllPermissionsGranted() || hasPartialAccess(activity)) { - onPermissionGranted.run(); - return; - } - if (report.isAnyPermissionPermanentlyDenied()) { - // permission is denied permanently, we will show user a dialog message. - DialogUtil.showAlertDialog( - activity, activity.getString(rationaleTitle), - activity.getString(rationaleMessage), - activity.getString(R.string.navigation_item_settings), - null, () -> { - askUserToManuallyEnablePermissionFromSettings(activity); - if (activity instanceof UploadActivity) { - ((UploadActivity) activity).setShowPermissionsDialog(true); - } - }, null, null, - !(activity instanceof UploadActivity)); - } else { - if (null != onPermissionDenied) { - onPermissionDenied.run(); - } - } - } - - @Override - public void onPermissionRationaleShouldBeShown( - final List permissions, - final PermissionToken token - ) { - if (rationaleTitle == -1 && rationaleMessage == -1) { - token.continuePermissionRequest(); - return; - } - DialogUtil.showAlertDialog( - activity, activity.getString(rationaleTitle), - activity.getString(rationaleMessage), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), - () -> { - if (activity instanceof UploadActivity) { - ((UploadActivity) activity).setShowPermissionsDialog(true); - } - token.continuePermissionRequest(); - }, - () -> { - Toast.makeText(activity.getApplicationContext(), - R.string.permissions_are_required_for_functionality, - Toast.LENGTH_LONG - ).show(); - token.cancelPermissionRequest(); - if (activity instanceof UploadActivity) { - activity.finish(); - } - }, null, false - ); - } - }).onSameThread().check(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt new file mode 100644 index 0000000000..305388fab7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.kt @@ -0,0 +1,231 @@ +package fr.free.nrw.commons.utils + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.widget.Toast +import androidx.core.content.ContextCompat +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.PermissionToken +import com.karumi.dexter.listener.PermissionRequest +import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.upload.UploadActivity + + +object PermissionUtils { + + @JvmStatic + val PERMISSIONS_STORAGE: Array = getPermissionsStorage() + + @JvmStatic + private fun getPermissionsStorage(): Array { + return when { + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> arrayOf( + Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED, + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT == Build.VERSION_CODES.TIRAMISU -> arrayOf( + Manifest.permission.READ_MEDIA_IMAGES, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT > Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + Build.VERSION.SDK_INT == Build.VERSION_CODES.Q -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.ACCESS_MEDIA_LOCATION + ) + else -> arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + } + } + + /** + * This method can be used by any activity which requires a permission which has been + * blocked(marked never ask again by the user) It open the app settings from where the user can + * manually give us the required permission. + * + * @param activity The Activity which requires a permission which has been blocked + */ + @JvmStatic + private fun askUserToManuallyEnablePermissionFromSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.fromParts("package", activity.packageName, null) + } + activity.startActivity(intent) + } + + /** + * Checks whether the app already has a particular permission + * + * @param activity The Activity context to check permissions against + * @param permissions An array of permission strings to check + * @return `true if the app has all the specified permissions, `false` otherwise + */ + @JvmStatic + fun hasPermission(activity: Activity, permissions: Array): Boolean { + return permissions.all { permission -> + ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED + } + } + + /** + * Check if the app has partial access permissions. + */ + @JvmStatic + fun hasPartialAccess(activity: Activity): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED + ) == PackageManager.PERMISSION_GRANTED && + ContextCompat.checkSelfPermission( + activity, Manifest.permission.READ_MEDIA_IMAGES + ) == PackageManager.PERMISSION_DENIED + } else false + } + + /** + * Checks for a particular permission and runs the runnable to perform an action when the + * permission is granted Also, it shows a rationale if needed + *

+ * rationaleTitle and rationaleMessage can be invalid @StringRes. If the value is -1 then no + * permission rationale will be displayed and permission would be requested + *

+ * Sample usage: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), + * R.string.storage_permission_title, R.string.write_storage_permission_rationale); + *

+ * If you don't want the permission rationale to be shown then use: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), - 1, -1); + * + * @param activity activity requesting permissions + * @param permissions the permissions array being requests + * @param onPermissionGranted the runnable to be executed when the permission is granted + * @param rationaleTitle rationale title to be displayed when permission was denied. It can + * be an invalid @StringRes + * @param rationaleMessage rationale message to be displayed when permission was denied. It + * can be an invalid @StringRes + */ + @JvmStatic + fun checkPermissionsAndPerformAction( + activity: Activity, + onPermissionGranted: Runnable, + rationaleTitle: Int, + rationaleMessage: Int, + vararg permissions: String + ) { + if (hasPartialAccess(activity)) { + Thread(onPermissionGranted).start() + return + } + checkPermissionsAndPerformAction( + activity, onPermissionGranted, null, rationaleTitle, rationaleMessage, *permissions + ) + } + + /** + * Checks for a particular permission and runs the corresponding runnables to perform an action + * when the permission is granted/denied Also, it shows a rationale if needed + *

+ * Sample usage: + *

+ * PermissionUtils.checkPermissionsAndPerformAction(activity, + * Manifest.permission.WRITE_EXTERNAL_STORAGE, () -> initiateCameraUpload(activity), () -> + * showMessage(), R.string.storage_permission_title, + * R.string.write_storage_permission_rationale); + * + * @param activity activity requesting permissions + * @param permissions the permissions array being requested + * @param onPermissionGranted the runnable to be executed when the permission is granted + * @param onPermissionDenied the runnable to be executed when the permission is denied(but not + * permanently) + * @param rationaleTitle rationale title to be displayed when permission was denied + * @param rationaleMessage rationale message to be displayed when permission was denied + */ + @JvmStatic + fun checkPermissionsAndPerformAction( + activity: Activity, + onPermissionGranted: Runnable, + onPermissionDenied: Runnable? = null, + rationaleTitle: Int, + rationaleMessage: Int, + vararg permissions: String + ) { + Dexter.withActivity(activity) + .withPermissions(*permissions) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + when { + report.areAllPermissionsGranted() || hasPartialAccess(activity) -> + Thread(onPermissionGranted).start() + report.isAnyPermissionPermanentlyDenied -> { + DialogUtil.showAlertDialog( + activity, + activity.getString(rationaleTitle), + activity.getString(rationaleMessage), + activity.getString(R.string.navigation_item_settings), + null, + { + askUserToManuallyEnablePermissionFromSettings(activity) + if (activity is UploadActivity) { + activity.isShowPermissionsDialog = true + } + }, + null, null, activity !is UploadActivity + ) + } + else -> Thread(onPermissionDenied).start() + } + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, token: PermissionToken + ) { + if (rationaleTitle == -1 && rationaleMessage == -1) { + token.continuePermissionRequest() + return + } + DialogUtil.showAlertDialog( + activity, + activity.getString(rationaleTitle), + activity.getString(rationaleMessage), + activity.getString(android.R.string.ok), + activity.getString(android.R.string.cancel), + { + if (activity is UploadActivity) { + activity.setShowPermissionsDialog(true) + } + token.continuePermissionRequest() + }, + { + Toast.makeText( + activity.applicationContext, + R.string.permissions_are_required_for_functionality, + Toast.LENGTH_LONG + ).show() + token.cancelPermissionRequest() + if (activity is UploadActivity) { + activity.finish() + } + }, + null, false + ) + } + }).onSameThread().check() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java deleted file mode 100644 index f1022a0418..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.java +++ /dev/null @@ -1,55 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.Sitelinks; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import fr.free.nrw.commons.location.LatLng; - -public class PlaceUtils { - - public static LatLng latLngFromPointString(String pointString) { - double latitude; - double longitude; - Matcher matcher = Pattern.compile("Point\\(([^ ]+) ([^ ]+)\\)").matcher(pointString); - if (!matcher.find()) { - return null; - } - try { - longitude = Double.parseDouble(matcher.group(1)); - latitude = Double.parseDouble(matcher.group(2)); - } catch (NumberFormatException e) { - return null; - } - - return new LatLng(latitude, longitude, 0); - } - - /** - * Turns a Media list to a Place list by creating a new list in Place type - * @param mediaList - * @return - */ - public static List mediaToExplorePlace( List mediaList) { - List explorePlaceList = new ArrayList<>(); - for (Media media :mediaList) { - explorePlaceList.add(new Place(media.getFilename(), - media.getFallbackDescription(), - media.getCoordinates(), - media.getCategories().toString(), - new Sitelinks.Builder() - .setCommonsLink(media.getPageTitle().getCanonicalUri()) - .setWikipediaLink("") // we don't necessarily have them, can be fetched later - .setWikidataLink("") // we don't necessarily have them, can be fetched later - .build(), - media.getImageUrl(), - media.getThumbUrl(), - "")); - } - return explorePlaceList; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt new file mode 100644 index 0000000000..907420f21f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/PlaceUtils.kt @@ -0,0 +1,50 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.Sitelinks + +object PlaceUtils { + + @JvmStatic + fun latLngFromPointString(pointString: String): LatLng? { + val matcher = Regex("Point\\(([^ ]+) ([^ ]+)\\)").find(pointString) ?: return null + return try { + val longitude = matcher.groupValues[1].toDouble() + val latitude = matcher.groupValues[2].toDouble() + LatLng(latitude, longitude, 0.0F) + } catch (e: NumberFormatException) { + null + } + } + + /** + * Turns a Media list to a Place list by creating a new list in Place type + * @param mediaList + * @return + */ + @JvmStatic + fun mediaToExplorePlace(mediaList: List): List { + val explorePlaceList = mutableListOf() + for (media in mediaList) { + explorePlaceList.add( + Place( + media.filename, + media.fallbackDescription, + media.coordinates, + media.categories.toString(), + Sitelinks.Builder() + .setCommonsLink(media.pageTitle.canonicalUri) + .setWikipediaLink("") // we don't necessarily have them, can be fetched later + .setWikidataLink("") // we don't necessarily have them, can be fetched later + .build(), + media.imageUrl, + media.thumbUrl, + "" + ) + ) + } + return explorePlaceList + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java deleted file mode 100644 index 3144679720..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.utils; - -import fr.free.nrw.commons.category.CategoryItem; -import java.util.Comparator; - -public class StringSortingUtils { - - private StringSortingUtils() { - //no-op - } - - /** - * Returns Comparator for sorting strings by their similarity to the filter. - * By using this Comparator we get results - * from the highest to the lowest similarity with the filter. - * - * @param filter String to compare similarity with - * @return Comparator with string similarity - */ - public static Comparator sortBySimilarity(final String filter) { - return (firstItem, secondItem) -> { - double firstItemSimilarity = calculateSimilarity(firstItem.getName(), filter); - double secondItemSimilarity = calculateSimilarity(secondItem.getName(), filter); - return (int) Math.signum(secondItemSimilarity - firstItemSimilarity); - }; - } - - - /** - * Determines String similarity between str1 and str2 on scale from 0.0 to 1.0 - * @param str1 String 1 - * @param str2 String 2 - * @return Double between 0.0 and 1.0 that reflects string similarity - */ - private static double calculateSimilarity(String str1, String str2) { - int longerLength = Math.max(str1.length(), str2.length()); - - if (longerLength == 0) return 1.0; - - int distanceBetweenStrings = levenshteinDistance(str1, str2); - return (longerLength - distanceBetweenStrings) / (double) longerLength; - } - - /** - * Levershtein distance algorithm - * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java - * - * @param str1 String 1 - * @param str2 String 2 - * @return Number of characters the strings differ by - */ - private static int levenshteinDistance(String str1, String str2) { - if (str1.equals(str2)) return 0; - if (str1.length() == 0) return str2.length(); - if (str2.length() == 0) return str1.length(); - - int[] cost = new int[str1.length() + 1]; - int[] newcost = new int[str1.length() + 1]; - - // initial cost of skipping prefix in str1 - for (int i = 0; i < cost.length; i++) cost[i] = i; - - // transformation cost for each letter in str2 - for (int j = 1; j <= str2.length(); j++) { - // initial cost of skipping prefix in String str2 - newcost[0] = j; - - // transformation cost for each letter in str1 - for(int i = 1; i < cost.length; i++) { - // matching current letters in both strings - int match = (str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 : 1; - - // computing cost for each transformation - int cost_replace = cost[i - 1] + match; - int cost_insert = cost[i] + 1; - int cost_delete = newcost[i - 1] + 1; - - // keep minimum cost - newcost[i] = Math.min(Math.min(cost_insert, cost_delete), cost_replace); - } - - int[] tmp = cost; - cost = newcost; - newcost = tmp; - } - - // the distance is the cost for transforming all letters in both strings - return cost[str1.length()]; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt new file mode 100644 index 0000000000..d9f813ae04 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringSortingUtils.kt @@ -0,0 +1,86 @@ +package fr.free.nrw.commons.utils + +import fr.free.nrw.commons.category.CategoryItem +import java.lang.Math.signum +import java.util.Comparator + + +object StringSortingUtils { + + /** + * Returns Comparator for sorting strings by their similarity to the filter. + * By using this Comparator we get results + * from the highest to the lowest similarity with the filter. + * + * @param filter String to compare similarity with + * @return Comparator with string similarity + */ + @JvmStatic + fun sortBySimilarity(filter: String): Comparator { + return Comparator { firstItem, secondItem -> + val firstItemSimilarity = calculateSimilarity(firstItem.name, filter) + val secondItemSimilarity = calculateSimilarity(secondItem.name, filter) + signum(secondItemSimilarity - firstItemSimilarity).toInt() + } + } + + /** + * Determines String similarity between str1 and str2 on scale from 0.0 to 1.0 + * @param str1 String 1 + * @param str2 String 2 + * @return Double between 0.0 and 1.0 that reflects string similarity + */ + private fun calculateSimilarity(str1: String, str2: String): Double { + val longerLength = maxOf(str1.length, str2.length) + + if (longerLength == 0) return 1.0 + + val distanceBetweenStrings = levenshteinDistance(str1, str2) + return (longerLength - distanceBetweenStrings) / longerLength.toDouble() + } + + /** + * Levenshtein distance algorithm + * https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Java + * + * @param str1 String 1 + * @param str2 String 2 + * @return Number of characters the strings differ by + */ + private fun levenshteinDistance(str1: String, str2: String): Int { + if (str1 == str2) return 0 + if (str1.isEmpty()) return str2.length + if (str2.isEmpty()) return str1.length + + var cost = IntArray(str1.length + 1) { it } + var newCost = IntArray(str1.length + 1) + + // transformation cost for each letter in str2 + for (j in 1..str2.length) { + // initial cost of skipping prefix in String str2 + newCost[0] = j + + // transformation cost for each letter in str1 + for (i in 1..str1.length) { + // matching current letters in both strings + val match = if (str1[i - 1] == str2[j - 1]) 0 else 1 + + // computing cost for each transformation + val costReplace = cost[i - 1] + match + val costInsert = cost[i] + 1 + val costDelete = newCost[i - 1] + 1 + + // keep minimum cost + newCost[i] = minOf(costInsert, costDelete, costReplace) + } + + // swap cost arrays + val tmp = cost + cost = newCost + newCost = tmp + } + + // the distance is the cost for transforming all letters in both strings + return cost[str1.length] + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java deleted file mode 100644 index a5bb6038e9..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Build; -import android.text.Html; -import android.text.Spanned; -import android.text.SpannedString; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -public final class StringUtil { - - /** - * @param source String that may contain HTML tags. - * @return returned Spanned string that may contain spans parsed from the HTML source. - */ - @NonNull public static Spanned fromHtml(@Nullable String source) { - if (source == null) { - return new SpannedString(""); - } - if (!source.contains("<") && !source.contains("&")) { - // If the string doesn't contain any hints of HTML entities, then skip the expensive - // processing that fromHtml() performs. - return new SpannedString(source); - } - source = source.replaceAll("‎", "\u200E") - .replaceAll("‏", "\u200F") - .replaceAll("&", "&"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return Html.fromHtml(source, Html.FROM_HTML_MODE_LEGACY); - } else { - //noinspection deprecation - return Html.fromHtml(source); - } - } - - private StringUtil() { - } -} 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 new file mode 100644 index 0000000000..b3c58d8b29 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/StringUtil.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.utils + +import android.os.Build +import android.text.Html +import android.text.Spanned +import android.text.SpannedString + +object StringUtil { + + /** + * @param source String that may contain HTML tags. + * @return returned Spanned string that may contain spans parsed from the HTML source. + */ + @JvmStatic + fun fromHtml(source: String?): Spanned { + if (source == null) { + return SpannedString("") + } + if (!source.contains("<") && !source.contains("&")) { + // If the string doesn't contain any hints of HTML entities, then skip the expensive + // processing that fromHtml() performs. + return SpannedString(source) + } + val processedSource = source + .replace("‎", "\u200E") + .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) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java deleted file mode 100644 index 7ea7ef467e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.java +++ /dev/null @@ -1,74 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.res.Resources; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.cardview.widget.CardView; - -import timber.log.Timber; - -/** - * A card view which informs onSwipe events to its child - */ -public abstract class SwipableCardView extends CardView { - float x1, x2; - private static final float MINIMUM_THRESHOLD_FOR_SWIPE = 100; - - public SwipableCardView(@NonNull Context context) { - super(context); - interceptOnTouchListener(); - } - - public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - interceptOnTouchListener(); - } - - public SwipableCardView(@NonNull Context context, @Nullable AttributeSet attrs, - int defStyleAttr) { - super(context, attrs, defStyleAttr); - interceptOnTouchListener(); - } - - private void interceptOnTouchListener() { - this.setOnTouchListener((v, event) -> { - boolean isSwipe = false; - float deltaX = 0.0f; - Timber.e(event.getAction() + ""); - switch (event.getAction()) { - case MotionEvent.ACTION_DOWN: - x1 = event.getX(); - break; - case MotionEvent.ACTION_UP: - x2 = event.getX(); - deltaX = x2 - x1; - if (deltaX < 0) { - //Right to left swipe - isSwipe = true; - } else if (deltaX > 0) { - //Left to right swipe - isSwipe = true; - } - break; - } - if (isSwipe && (pixelToDp(Math.abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE)) { - return onSwipe(v); - } - return false; - }); - } - - /** - * abstract function which informs swipe events to those who have inherited from it - */ - public abstract boolean onSwipe(View view); - - private float pixelToDp(float pixels) { - return (pixels / Resources.getSystem().getDisplayMetrics().density); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt new file mode 100644 index 0000000000..5a8261c24f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SwipableCardView.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.utils + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.Resources +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View + +import androidx.cardview.widget.CardView + +import timber.log.Timber +import kotlin.math.abs + +/** + * A card view which informs onSwipe events to its child + */ +abstract class SwipableCardView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : CardView(context, attrs, defStyleAttr) { + + private var x1 = 0f + private var x2 = 0f + private val MINIMUM_THRESHOLD_FOR_SWIPE = 100f + + init { + interceptOnTouchListener() + } + + @SuppressLint("ClickableViewAccessibility") + private fun interceptOnTouchListener() { + this.setOnTouchListener { v, event -> + var isSwipe = false + var deltaX = 0f + Timber.e(event.action.toString()) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + x1 = event.x + } + MotionEvent.ACTION_UP -> { + x2 = event.x + deltaX = x2 - x1 + isSwipe = deltaX != 0f + } + } + if (isSwipe && pixelToDp(abs(deltaX)) > MINIMUM_THRESHOLD_FOR_SWIPE) { + onSwipe(v) + return@setOnTouchListener true + } + false + } + } + + /** + * abstract function which informs swipe events to those who have inherited from it + */ + abstract fun onSwipe(view: View): Boolean + + private fun pixelToDp(pixels: Float): Float { + return pixels / Resources.getSystem().displayMetrics.density + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java deleted file mode 100644 index aa60a7aa85..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.java +++ /dev/null @@ -1,49 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.content.res.Configuration; - -import javax.inject.Inject; -import javax.inject.Named; - -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.settings.Prefs; - -public class SystemThemeUtils { - - private Context context; - private JsonKvStore applicationKvStore; - - public static final String THEME_MODE_DEFAULT = "0"; - public static final String THEME_MODE_DARK = "1"; - public static final String THEME_MODE_LIGHT = "2"; - - @Inject - public SystemThemeUtils(Context context, @Named("default_preferences") JsonKvStore applicationKvStore) { - this.context = context; - this.applicationKvStore = applicationKvStore; - } - - // Return true is system wide dark theme is enabled else false - public boolean getSystemDefaultThemeBool(String theme) { - if (theme.equals(THEME_MODE_DARK)) { - return true; - } else if (theme.equals(THEME_MODE_DEFAULT)) { - return getSystemDefaultThemeBool(getSystemDefaultTheme()); - } - return false; - } - - // Returns the default system wide theme - public String getSystemDefaultTheme() { - return (context.getResources().getConfiguration().uiMode & - Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES ? THEME_MODE_DARK : THEME_MODE_LIGHT; - } - - // Returns true if the device is in night mode or false otherwise - public boolean isDeviceInNightMode() { - return getSystemDefaultThemeBool( - applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt new file mode 100644 index 0000000000..f4b1f2625d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.content.res.Configuration + +import javax.inject.Inject +import javax.inject.Named + +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.settings.Prefs + + +class SystemThemeUtils @Inject constructor( + private val context: Context, + @Named("default_preferences") private val applicationKvStore: JsonKvStore +) { + + companion object { + const val THEME_MODE_DEFAULT = "0" + const val THEME_MODE_DARK = "1" + const val THEME_MODE_LIGHT = "2" + } + + // Return true if system wide dark theme is enabled else false + private fun getSystemDefaultThemeBool(theme: String): Boolean { + return when (theme) { + THEME_MODE_DARK -> true + THEME_MODE_DEFAULT -> getSystemDefaultThemeBool(getSystemDefaultTheme()) + else -> false + } + } + + // Returns the default system wide theme + private fun getSystemDefaultTheme(): String { + return if ( + ( + context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) + == Configuration.UI_MODE_NIGHT_YES + ) { + THEME_MODE_DARK + } else { + THEME_MODE_LIGHT + } + } + + // Returns true if the device is in night mode or false otherwise + fun isDeviceInNightMode(): Boolean { + return getSystemDefaultThemeBool( + applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme()) + ) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java deleted file mode 100644 index acb6afbaa2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.java +++ /dev/null @@ -1,39 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.util.DisplayMetrics; - -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import java.util.ArrayList; -import java.util.List; - -public class UiUtils { - - /** - * Draws a vectorial image onto a bitmap. - * @param vectorDrawable vectorial image - * @return bitmap representation of the vectorial image - */ - public static Bitmap getBitmap(VectorDrawableCompat vectorDrawable) { - Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(), - vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); - vectorDrawable.draw(canvas); - return bitmap; - } - - /** - * Converts dp unit to equivalent pixels. - * @param dp density independent pixels - * @param context Context to access display metrics - * @return px equivalent to dp value - */ - public static float convertDpToPixel(float dp, Context context) { - DisplayMetrics metrics = context.getResources().getDisplayMetrics(); - return dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt new file mode 100644 index 0000000000..9ff069ebcd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/UiUtils.kt @@ -0,0 +1,41 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.util.DisplayMetrics +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat + + +object UiUtils { + + /** + * Draws a vectorial image onto a bitmap. + * @param vectorDrawable vectorial image + * @return bitmap representation of the vectorial image + */ + @JvmStatic + fun getBitmap(vectorDrawable: VectorDrawableCompat): Bitmap { + val bitmap = Bitmap.createBitmap( + vectorDrawable.intrinsicWidth, + vectorDrawable.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + val canvas = Canvas(bitmap) + vectorDrawable.setBounds(0, 0, canvas.width, canvas.height) + vectorDrawable.draw(canvas) + return bitmap + } + + /** + * Converts dp unit to equivalent pixels. + * @param dp density independent pixels + * @param context Context to access display metrics + * @return px equivalent to dp value + */ + @JvmStatic + fun convertDpToPixel(dp: Float, context: Context): Float { + val metrics = context.resources.displayMetrics + return dp * (metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT.toFloat()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java deleted file mode 100644 index 1272dc4f18..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ /dev/null @@ -1,143 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.app.Activity; -import android.content.Context; -import android.graphics.Color; -import android.view.Display; -import android.view.View; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.StringRes; - -import androidx.core.content.ContextCompat; -import com.google.android.material.snackbar.Snackbar; - -import fr.free.nrw.commons.R; -import timber.log.Timber; - -public class ViewUtil { - /** - * Utility function to show short snack bar - * @param view - * @param messageResourceId - */ - public static void showShortSnackbar(View view, int messageResourceId) { - if (view.getContext() == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> { - try { - Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); - }catch (IllegalStateException e){ - Timber.e(e.getMessage()); - } - }); - } - public static void showLongSnackbar(View view, String text) { - if(view.getContext() == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(()-> { - try { - Snackbar snackbar = Snackbar.make(view, text, Snackbar.LENGTH_SHORT); - - View snack_view = snackbar.getView(); - TextView snack_text = snack_view.findViewById(R.id.snackbar_text); - - snack_view.setBackgroundColor(Color.LTGRAY); - snack_text.setTextColor(ContextCompat.getColor(view.getContext(), R.color.primaryColor)); - snackbar.setActionTextColor(Color.RED); - - snackbar.setAction("Dismiss", new View.OnClickListener() { - @Override - public void onClick(View v) { - // Handle the action click - snackbar.dismiss(); - } - }); - - snackbar.show(); - - }catch (IllegalStateException e) { - Timber.e(e.getMessage()); - } - }); - } - - public static void showLongToast(Context context, String text) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); - } - - public static void showLongToast(Context context, @StringRes int stringResourceId) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show()); - } - - public static void showShortToast(Context context, String text) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show()); - } - - public static void showShortToast(Context context, @StringRes int stringResourceId) { - if (context == null) { - return; - } - - ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show()); - } - - public static boolean isPortrait(Context context) { - Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay(); - if (orientation.getWidth() < orientation.getHeight()){ - return true; - } else { - return false; - } - } - - public static void hideKeyboard(View view){ - if (view != null) { - InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - view.clearFocus(); - if (manager != null) { - manager.hideSoftInputFromWindow(view.getWindowToken(), 0); - } - } - } - - /** - * A snack bar which has an action button which on click dismisses the snackbar and invokes the - * listener passed - */ - public static void showDismissibleSnackBar(View view, - int messageResourceId, - int actionButtonResourceId, - View.OnClickListener onClickListener) { - if (view.getContext() == null) { - return; - } - ExecutorUtils.uiExecutor().execute(() -> { - Snackbar snackbar = Snackbar.make(view, view.getContext().getString(messageResourceId), - Snackbar.LENGTH_INDEFINITE); - snackbar.setAction(view.getContext().getString(actionButtonResourceId), v -> { - snackbar.dismiss(); - onClickListener.onClick(v); - }); - snackbar.show(); - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt new file mode 100644 index 0000000000..64970ecf6f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.kt @@ -0,0 +1,151 @@ +package fr.free.nrw.commons.utils + +import android.app.Activity +import android.content.Context +import android.graphics.Color +import android.view.Display +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import android.widget.Toast + +import androidx.annotation.StringRes + +import androidx.core.content.ContextCompat +import com.google.android.material.snackbar.Snackbar + +import fr.free.nrw.commons.R +import timber.log.Timber + + +object ViewUtil { + + /** + * Utility function to show short snack bar + * @param view + * @param messageResourceId + */ + @JvmStatic + fun showShortSnackbar(view: View, messageResourceId: Int) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + try { + Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show() + } catch (e: IllegalStateException) { + Timber.e(e.message) + } + } + } + + @JvmStatic + fun showLongSnackbar(view: View, text: String) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + try { + val snackbar = Snackbar.make(view, text, Snackbar.LENGTH_SHORT) + val snackView = snackbar.view + val snackText: TextView = snackView.findViewById(R.id.snackbar_text) + + snackView.setBackgroundColor(Color.LTGRAY) + snackText.setTextColor(ContextCompat.getColor(view.context, R.color.primaryColor)) + snackbar.setActionTextColor(Color.RED) + + snackbar.setAction("Dismiss") { snackbar.dismiss() } + snackbar.show() + + } catch (e: IllegalStateException) { + Timber.e(e.message) + } + } + } + + @JvmStatic + fun showLongToast(context: Context, text: String) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, text, Toast.LENGTH_LONG).show() + } + } + + @JvmStatic + fun showLongToast(context: Context, @StringRes stringResourceId: Int) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show() + } + } + + @JvmStatic + fun showShortToast(context: Context, text: String) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + fun showShortToast(context: Context?, @StringRes stringResourceId: Int) { + if (context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show() + } + } + + @JvmStatic + fun isPortrait(context: Context): Boolean { + val orientation = (context as Activity).windowManager.defaultDisplay + return orientation.width < orientation.height + } + + @JvmStatic + fun hideKeyboard(view: View?) { + view?.let { + val manager = it.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + it.clearFocus() + manager?.hideSoftInputFromWindow(it.windowToken, 0) + } + } + + /** + * A snack bar which has an action button which on click dismisses the snackbar and invokes the + * listener passed + */ + @JvmStatic + fun showDismissibleSnackBar( + view: View, + messageResourceId: Int, + actionButtonResourceId: Int, + onClickListener: View.OnClickListener + ) { + if (view.context == null) { + return + } + + ExecutorUtils.uiExecutor().execute { + val snackbar = Snackbar.make(view, view.context.getString(messageResourceId), Snackbar.LENGTH_INDEFINITE) + snackbar.setAction(view.context.getString(actionButtonResourceId)) { + snackbar.dismiss() + onClickListener.onClick(it) + } + snackbar.show() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java deleted file mode 100644 index 2721ef98dc..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.content.Context; - -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton -public class ViewUtilWrapper { - - @Inject - public ViewUtilWrapper() { - - } - - public void showShortToast(Context context, String text) { - ViewUtil.showShortToast(context, text); - } - - public void showLongToast(Context context, String text) { - ViewUtil.showLongToast(context, text); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt new file mode 100644 index 0000000000..b5ead3041c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtilWrapper.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.utils + +import android.content.Context +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ViewUtilWrapper @Inject constructor() { + + fun showShortToast(context: Context, text: String) { + ViewUtil.showShortToast(context, text) + } + + fun showLongToast(context: Context, text: String) { + ViewUtil.showLongToast(context, text) + } +} From cb4ffd8ca87baf1177bd56ac0d2b9935b0f8e19e Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 20 Nov 2024 09:11:50 +0530 Subject: [PATCH 025/231] Migrated widget module from Java to Kotlin (#5940) * Rename .java to .kt * Migrated widget module to Kotlin --- .../di/CommonsApplicationComponent.java | 1 - .../widget/HeightLimitedRecyclerView.java | 48 ----- .../widget/HeightLimitedRecyclerView.kt | 43 +++++ .../nrw/commons/widget/PicOfDayAppWidget.java | 181 ------------------ .../nrw/commons/widget/PicOfDayAppWidget.kt | 174 +++++++++++++++++ .../free/nrw/commons/widget/ViewHolder.java | 7 - .../fr/free/nrw/commons/widget/ViewHolder.kt | 7 + 7 files changed, 224 insertions(+), 237 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java create mode 100644 app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java create mode 100644 app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 1390bd8ef4..0d847b6493 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -2,7 +2,6 @@ import com.google.gson.Gson; -import fr.free.nrw.commons.actions.PageEditClient; import fr.free.nrw.commons.explore.categories.CategoriesModule; import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; diff --git a/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java deleted file mode 100644 index 5c6dde5fd2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.widget; - -import android.app.Activity; -import android.content.Context; -import android.util.AttributeSet; -import android.util.DisplayMetrics; - -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - -/** - * Created by Ilgaz Er on 8/7/2018. - */ -public class HeightLimitedRecyclerView extends RecyclerView { - int height; - public HeightLimitedRecyclerView(Context context) { - super(context); - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager() - .getDefaultDisplay() - .getMetrics(displayMetrics); - height=displayMetrics.heightPixels; - } - - public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager() - .getDefaultDisplay() - .getMetrics(displayMetrics); - height=displayMetrics.heightPixels; - } - - public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - DisplayMetrics displayMetrics = new DisplayMetrics(); - ((Activity) getContext()).getWindowManager() - .getDefaultDisplay() - .getMetrics(displayMetrics); - height=displayMetrics.heightPixels; - } - - @Override - protected void onMeasure(int widthSpec, int heightSpec) { - heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST); - super.onMeasure(widthSpec, heightSpec); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt new file mode 100644 index 0000000000..b864552435 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/HeightLimitedRecyclerView.kt @@ -0,0 +1,43 @@ +package fr.free.nrw.commons.widget + +import android.app.Activity +import android.content.Context +import android.util.AttributeSet +import android.util.DisplayMetrics + +import androidx.annotation.Nullable +import androidx.recyclerview.widget.RecyclerView + + +/** + * Created by Ilgaz Er on 8/7/2018. + */ +class HeightLimitedRecyclerView : RecyclerView { + private var height: Int = 0 + + constructor(context: Context) : super(context) { + initializeHeight(context) + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + initializeHeight(context) + } + + constructor(context: Context, attrs: AttributeSet?, defStyle: Int) : super(context, attrs, defStyle) { + initializeHeight(context) + } + + private fun initializeHeight(context: Context) { + val displayMetrics = DisplayMetrics() + (context as Activity).windowManager.defaultDisplay.getMetrics(displayMetrics) + height = displayMetrics.heightPixels + } + + override fun onMeasure(widthSpec: Int, heightSpec: Int) { + val limitedHeightSpec = MeasureSpec.makeMeasureSpec( + (height * 0.3).toInt(), + MeasureSpec.AT_MOST + ) + super.onMeasure(widthSpec, limitedHeightSpec) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java deleted file mode 100644 index 2734520787..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java +++ /dev/null @@ -1,181 +0,0 @@ -package fr.free.nrw.commons.widget; - -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.net.Uri; -import android.os.Build; -import android.widget.RemoteViews; -import androidx.annotation.Nullable; -import com.facebook.common.executors.CallerThreadExecutor; -import com.facebook.common.references.CloseableReference; -import com.facebook.datasource.DataSource; -import com.facebook.drawee.backends.pipeline.Fresco; -import com.facebook.imagepipeline.core.ImagePipeline; -import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; -import com.facebook.imagepipeline.image.CloseableImage; -import com.facebook.imagepipeline.request.ImageRequest; -import com.facebook.imagepipeline.request.ImageRequestBuilder; -import fr.free.nrw.commons.media.MediaClient; -import javax.inject.Inject; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -import static android.content.Intent.ACTION_VIEW; - -/** - * Implementation of App Widget functionality. - */ -public class PicOfDayAppWidget extends AppWidgetProvider { - - private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - - @Inject - MediaClient mediaClient; - - void updateAppWidget( - final Context context, - final AppWidgetManager appWidgetManager, - final int appWidgetId - ) { - final RemoteViews views = new RemoteViews( - context.getPackageName(), R.layout.pic_of_day_app_widget); - - // Launch App on Button Click - final Intent viewIntent = new Intent(context, MainActivity.class); - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; - } - final PendingIntent pendingIntent = PendingIntent.getActivity( - context, 0, viewIntent, flags); - - views.setOnClickPendingIntent(R.id.camera_button, pendingIntent); - appWidgetManager.updateAppWidget(appWidgetId, views); - - loadPictureOfTheDay(context, views, appWidgetManager, appWidgetId); - } - - /** - * Loads the picture of the day using media wiki API - * @param context The application context. - * @param views The RemoteViews object used to update the App Widget UI. - * @param appWidgetManager The AppWidgetManager instance for managing the widget. - * @param appWidgetId he ID of the App Widget to update. - */ - private void loadPictureOfTheDay( - final Context context, - final RemoteViews views, - final AppWidgetManager appWidgetManager, - final int appWidgetId - ) { - compositeDisposable.add(mediaClient.getPictureOfTheDay() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - views.setTextViewText(R.id.appwidget_title, response.getDisplayTitle()); - - // View in browser - final Intent viewIntent = new Intent(); - viewIntent.setAction(ACTION_VIEW); - viewIntent.setData(Uri.parse(response.getPageTitle().getMobileUri())); - - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT>= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; - } - final PendingIntent pendingIntent = PendingIntent.getActivity( - context, 0, viewIntent, flags); - - views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent); - loadImageFromUrl(response.getThumbUrl(), - context, views, appWidgetManager, appWidgetId); - } - }, - t -> Timber.e(t, "Fetching picture of the day failed") - )); - } - - /** - * Uses Fresco to load an image from Url - * @param imageUrl The URL of the image to load. - * @param context The application context. - * @param views The RemoteViews object used to update the App Widget UI. - * @param appWidgetManager The AppWidgetManager instance for managing the widget. - * @param appWidgetId he ID of the App Widget to update. - */ - private void loadImageFromUrl( - final String imageUrl, - final Context context, - final RemoteViews views, - final AppWidgetManager appWidgetManager, - final int appWidgetId - ) { - final ImageRequest request = ImageRequestBuilder - .newBuilderWithSource(Uri.parse(imageUrl)).build(); - final ImagePipeline imagePipeline = Fresco.getImagePipeline(); - final DataSource> dataSource = imagePipeline - .fetchDecodedImage(request, context); - - dataSource.subscribe(new BaseBitmapDataSubscriber() { - @Override - protected void onNewResultImpl(@Nullable final Bitmap tempBitmap) { - Bitmap bitmap = null; - if (tempBitmap != null) { - bitmap = Bitmap.createBitmap( - tempBitmap.getWidth(), tempBitmap.getHeight(), Bitmap.Config.ARGB_8888 - ); - final Canvas canvas = new Canvas(bitmap); - canvas.drawBitmap(tempBitmap, 0f, 0f, new Paint()); - } - views.setImageViewBitmap(R.id.appwidget_image, bitmap); - appWidgetManager.updateAppWidget(appWidgetId, views); - } - - @Override - protected void onFailureImpl( - final DataSource> dataSource - ) { - // Ignore failure for now. - } - }, CallerThreadExecutor.getInstance()); - } - - @Override - public void onUpdate( - final Context context, - final AppWidgetManager appWidgetManager, - final int[] appWidgetIds - ) { - ApplicationlessInjection - .getInstance(context.getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - // There may be multiple widgets active, so update all of them - for (final int appWidgetId : appWidgetIds) { - updateAppWidget(context, appWidgetManager, appWidgetId); - } - } - - @Override - public void onEnabled(final Context context) { - // Enter relevant functionality for when the first widget is created - } - - @Override - public void onDisabled(final Context context) { - // Enter relevant functionality for when the last widget is disabled - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt new file mode 100644 index 0000000000..ab6a45b857 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.kt @@ -0,0 +1,174 @@ +package fr.free.nrw.commons.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.net.Uri +import android.os.Build +import android.widget.RemoteViews +import androidx.annotation.Nullable +import com.facebook.common.executors.CallerThreadExecutor +import com.facebook.common.references.CloseableReference +import com.facebook.datasource.DataSource +import com.facebook.drawee.backends.pipeline.Fresco +import com.facebook.imagepipeline.core.ImagePipeline +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber +import com.facebook.imagepipeline.image.CloseableImage +import com.facebook.imagepipeline.request.ImageRequest +import com.facebook.imagepipeline.request.ImageRequestBuilder +import fr.free.nrw.commons.media.MediaClient +import javax.inject.Inject +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.di.ApplicationlessInjection +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + +/** + * Implementation of App Widget functionality. + */ +class PicOfDayAppWidget : AppWidgetProvider() { + + private val compositeDisposable = CompositeDisposable() + + @Inject + lateinit var mediaClient: MediaClient + + private fun updateAppWidget( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + val views = RemoteViews(context.packageName, R.layout.pic_of_day_app_widget) + + // Launch App on Button Click + val viewIntent = Intent(context, MainActivity::class.java) + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + val pendingIntent = PendingIntent.getActivity(context, 0, viewIntent, flags) + views.setOnClickPendingIntent(R.id.camera_button, pendingIntent) + + appWidgetManager.updateAppWidget(appWidgetId, views) + + loadPictureOfTheDay(context, views, appWidgetManager, appWidgetId) + } + + /** + * Loads the picture of the day using media wiki API + * @param context The application context. + * @param views The RemoteViews object used to update the App Widget UI. + * @param appWidgetManager The AppWidgetManager instance for managing the widget. + * @param appWidgetId The ID of the App Widget to update. + */ + private fun loadPictureOfTheDay( + context: Context, + views: RemoteViews, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + compositeDisposable.add( + mediaClient.getPictureOfTheDay() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response != null) { + views.setTextViewText(R.id.appwidget_title, response.displayTitle) + + // View in browser + val viewIntent = Intent().apply { + action = Intent.ACTION_VIEW + data = Uri.parse(response.pageTitle.mobileUri) + } + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + val pendingIntent = PendingIntent.getActivity( + context, + 0, + viewIntent, + flags + ) + + views.setOnClickPendingIntent(R.id.appwidget_image, pendingIntent) + loadImageFromUrl( + response.thumbUrl, + context, + views, + appWidgetManager, + appWidgetId + ) + } + }, + { t -> Timber.e(t, "Fetching picture of the day failed") } + ) + ) + } + + /** + * Uses Fresco to load an image from Url + * @param imageUrl The URL of the image to load. + * @param context The application context. + * @param views The RemoteViews object used to update the App Widget UI. + * @param appWidgetManager The AppWidgetManager instance for managing the widget. + * @param appWidgetId The ID of the App Widget to update. + */ + private fun loadImageFromUrl( + imageUrl: String?, + context: Context, + views: RemoteViews, + appWidgetManager: AppWidgetManager, + appWidgetId: Int + ) { + val request = ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageUrl)).build() + val imagePipeline = Fresco.getImagePipeline() + val dataSource = imagePipeline.fetchDecodedImage(request, context) + + dataSource.subscribe(object : BaseBitmapDataSubscriber() { + override fun onNewResultImpl(tempBitmap: Bitmap?) { + val bitmap = tempBitmap?.let { + Bitmap.createBitmap(it.width, it.height, Bitmap.Config.ARGB_8888).apply { + Canvas(this).drawBitmap(it, 0f, 0f, Paint()) + } + } + views.setImageViewBitmap(R.id.appwidget_image, bitmap) + appWidgetManager.updateAppWidget(appWidgetId, views) + } + + override fun onFailureImpl(dataSource: DataSource>) { + // Ignore failure for now. + } + }, CallerThreadExecutor.getInstance()) + } + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { + ApplicationlessInjection + .getInstance(context.applicationContext) + .commonsApplicationComponent + .inject(this) + + // There may be multiple widgets active, so update all of them + for (appWidgetId in appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId) + } + } + + override fun onEnabled(context: Context) { + // Enter relevant functionality for when the first widget is created + } + + override fun onDisabled(context: Context) { + // Enter relevant functionality for when the last widget is disabled + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java b/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java deleted file mode 100644 index e2dd8d680e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.widget; - -import android.content.Context; - -public interface ViewHolder { - void bindModel(Context context, T model); -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt b/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt new file mode 100644 index 0000000000..f9f598b3e9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/ViewHolder.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.widget + +import android.content.Context + +interface ViewHolder { + fun bindModel(context: Context, model: T) +} \ No newline at end of file From ed18a37577dd08d56cbacb6da6050c4a107acfbb Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 20 Nov 2024 19:25:13 +0530 Subject: [PATCH 026/231] Migrated ui and theme modules from Java to Kotlin (#5942) * Rename .java to .kt * Migrated ui and theme module to Kotlin --- .../LocationPickerActivity.java | 4 +- .../ui/selector/ImageFragment.kt | 3 +- .../nrw/commons/explore/SearchActivity.java | 4 +- .../notification/NotificationActivity.java | 4 +- .../nrw/commons/profile/ProfileActivity.java | 2 +- .../free/nrw/commons/theme/BaseActivity.java | 66 ----------------- .../fr/free/nrw/commons/theme/BaseActivity.kt | 65 +++++++++++++++++ .../ui/PasteSensitiveTextInputEditText.java | 65 ----------------- .../ui/PasteSensitiveTextInputEditText.kt | 60 ++++++++++++++++ .../nrw/commons/ui/widget/HtmlTextView.java | 36 ---------- .../nrw/commons/ui/widget/HtmlTextView.kt | 32 +++++++++ .../nrw/commons/ui/widget/OverlayDialog.java | 70 ------------------- .../nrw/commons/ui/widget/OverlayDialog.kt | 64 +++++++++++++++++ 13 files changed, 230 insertions(+), 245 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java create mode 100644 app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java create mode 100644 app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java index 2f05705bac..40f360a243 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java @@ -367,7 +367,7 @@ private void onClickRemoveLocation() { */ private void removeLocationFromImage() { if (media != null) { - compositeDisposable.add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() + getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() , media, "0.0", "0.0", "0.0f") .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) @@ -479,7 +479,7 @@ public void updateCoordinates(final String Latitude, final String Longitude, } try { - compositeDisposable.add( + getCompositeDisposable().add( coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media, Latitude, Longitude, Accuracy) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt index dbab629ff0..3912a4d12f 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageFragment.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.customselector.ui.selector import android.app.Activity +import android.content.Context import android.content.Context.MODE_PRIVATE import android.content.SharedPreferences import android.os.Bundle @@ -346,7 +347,7 @@ class ImageFragment : context .getSharedPreferences( "CustomSelector", - BaseActivity.MODE_PRIVATE, + MODE_PRIVATE, )?.let { prefs -> prefs.edit()?.let { editor -> editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index abb27184f4..934bff6ece 100644 --- a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -104,7 +104,7 @@ public void setTabs() { viewPagerAdapter.setTabData(fragmentList, titleList); viewPagerAdapter.notifyDataSetChanged(); - compositeDisposable.add(RxSearchView.queryTextChanges(binding.searchBox) + getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox) .takeUntil(RxView.detaches(binding.searchBox)) .debounce(500, TimeUnit.MILLISECONDS) .observeOn(AndroidSchedulers.mainThread()) @@ -284,7 +284,7 @@ public void updateText(String query) { @Override protected void onDestroy() { super.onDestroy(); //Dispose the disposables when the activity is destroyed - compositeDisposable.dispose(); + getCompositeDisposable().dispose(); binding = null; } } diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index 572dd03171..b57df49485 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -133,7 +133,7 @@ public void removeNotification(Notification notification) { } binding.progressBar.setVisibility(View.GONE); }); - compositeDisposable.add(disposable); + getCompositeDisposable().add(disposable); } @@ -178,7 +178,7 @@ private void addNotifications(boolean archived) { Timber.d("Add notifications"); if (mNotificationWorkerFragment == null) { binding.progressBar.setVisibility(View.VISIBLE); - compositeDisposable.add(controller.getNotifications(archived) + getCompositeDisposable().add(controller.getNotifications(archived) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(notificationList -> { diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index c6d09fdc66..60a0f47a1a 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -157,7 +157,7 @@ private void setTabs() { @Override public void onDestroy() { super.onDestroy(); - compositeDisposable.clear(); + getCompositeDisposable().clear(); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java deleted file mode 100644 index 95ec00dc6f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.java +++ /dev/null @@ -1,66 +0,0 @@ -package fr.free.nrw.commons.theme; - -import android.content.res.Configuration; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.WindowManager; -import javax.inject.Inject; -import javax.inject.Named; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import io.reactivex.disposables.CompositeDisposable; - -public abstract class BaseActivity extends CommonsDaggerAppCompatActivity { - @Inject - @Named("default_preferences") - public JsonKvStore defaultKvStore; - - @Inject - SystemThemeUtils systemThemeUtils; - - protected CompositeDisposable compositeDisposable = new CompositeDisposable(); - protected boolean wasPreviouslyDarkTheme; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode(); - setTheme(wasPreviouslyDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme); - float fontScale = android.provider.Settings.System.getFloat( - getBaseContext().getContentResolver(), - android.provider.Settings.System.FONT_SCALE, - 1f); - adjustFontScale(getResources().getConfiguration(), fontScale); - } - - @Override - protected void onResume() { - // Restart activity if theme is changed - if (wasPreviouslyDarkTheme != systemThemeUtils.isDeviceInNightMode()) { - recreate(); - } - - super.onResume(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - } - - /** - * Apply fontScale on device - */ - public void adjustFontScale(Configuration configuration, float scale) { - configuration.fontScale = scale; - final DisplayMetrics metrics = getResources().getDisplayMetrics(); - final WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE); - wm.getDefaultDisplay().getMetrics(metrics); - metrics.scaledDensity = configuration.fontScale * metrics.density; - getBaseContext().getResources().updateConfiguration(configuration, metrics); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt new file mode 100644 index 0000000000..d2d9364603 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/theme/BaseActivity.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.theme + +import android.content.res.Configuration +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.WindowManager +import javax.inject.Inject +import javax.inject.Named +import fr.free.nrw.commons.R +import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.utils.SystemThemeUtils +import io.reactivex.disposables.CompositeDisposable + + +abstract class BaseActivity : CommonsDaggerAppCompatActivity() { + + @Inject + @field:Named("default_preferences") + lateinit var defaultKvStore: JsonKvStore + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + protected val compositeDisposable = CompositeDisposable() + protected var wasPreviouslyDarkTheme: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + wasPreviouslyDarkTheme = systemThemeUtils.isDeviceInNightMode() + setTheme(if (wasPreviouslyDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme) + + val fontScale = android.provider.Settings.System.getFloat( + baseContext.contentResolver, + android.provider.Settings.System.FONT_SCALE, + 1f + ) + adjustFontScale(resources.configuration, fontScale) + } + + override fun onResume() { + // Restart activity if theme is changed + if (wasPreviouslyDarkTheme != systemThemeUtils.isDeviceInNightMode()) { + recreate() + } + super.onResume() + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + /** + * Apply fontScale on device + */ + fun adjustFontScale(configuration: Configuration, scale: Float) { + configuration.fontScale = scale + val metrics = resources.displayMetrics + val wm = getSystemService(WINDOW_SERVICE) as WindowManager + wm.defaultDisplay.getMetrics(metrics) + metrics.scaledDensity = configuration.fontScale * metrics.density + baseContext.resources.updateConfiguration(configuration, metrics) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java b/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java deleted file mode 100644 index 0b82baf649..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.java +++ /dev/null @@ -1,65 +0,0 @@ -package fr.free.nrw.commons.ui; - -import android.content.Context; -import android.content.res.TypedArray; -import android.os.Build.VERSION; -import android.util.AttributeSet; -import com.google.android.material.textfield.TextInputEditText; -import fr.free.nrw.commons.R; - -public class PasteSensitiveTextInputEditText extends TextInputEditText { - - private boolean formattingAllowed = true; - - public PasteSensitiveTextInputEditText(final Context context) { - super(context); - } - - public PasteSensitiveTextInputEditText(final Context context, final AttributeSet attrs) { - super(context, attrs); - formattingAllowed = extractFormattingAttribute(context, attrs); - } - - @Override - public boolean onTextContextMenuItem(int id) { - - // if not paste command, or formatting is allowed, return default - if(id != android.R.id.paste || formattingAllowed){ - return super.onTextContextMenuItem(id); - } - - // if its paste and formatting not allowed - boolean proceeded; - if(VERSION.SDK_INT >= 23) { - proceeded = super.onTextContextMenuItem(android.R.id.pasteAsPlainText); - }else { - proceeded = super.onTextContextMenuItem(id); - if (proceeded && getText() != null) { - // rewrite with plain text so formatting is lost - setText(getText().toString()); - setSelection(getText().length()); - } - } - return proceeded; - } - - private boolean extractFormattingAttribute(Context context, AttributeSet attrs){ - - boolean formatAllowed = true; - - TypedArray a = context.getTheme().obtainStyledAttributes( - attrs, R.styleable.PasteSensitiveTextInputEditText, 0, 0); - - try { - formatAllowed = a.getBoolean( - R.styleable.PasteSensitiveTextInputEditText_allowFormatting, true); - } finally { - a.recycle(); - } - return formatAllowed; - } - - public void setFormattingAllowed(boolean formattingAllowed){ - this.formattingAllowed = formattingAllowed; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt b/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt new file mode 100644 index 0000000000..56f795485d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditText.kt @@ -0,0 +1,60 @@ +package fr.free.nrw.commons.ui + +import android.content.Context +import android.content.res.TypedArray +import android.os.Build +import android.os.Build.VERSION +import android.util.AttributeSet +import com.google.android.material.textfield.TextInputEditText +import fr.free.nrw.commons.R + + +class PasteSensitiveTextInputEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : TextInputEditText(context, attrs) { + + private var formattingAllowed: Boolean = true + + init { + if (attrs != null) { + formattingAllowed = extractFormattingAttribute(context, attrs) + } + } + + override fun onTextContextMenuItem(id: Int): Boolean { + // if not paste command, or formatting is allowed, return default + if (id != android.R.id.paste || formattingAllowed) { + return super.onTextContextMenuItem(id) + } + + // if it's paste and formatting not allowed + val proceeded: Boolean = if (VERSION.SDK_INT >= 23) { + super.onTextContextMenuItem(android.R.id.pasteAsPlainText) + } else { + val success = super.onTextContextMenuItem(id) + if (success && text != null) { + // rewrite with plain text so formatting is lost + setText(text.toString()) + setSelection(text?.length ?: 0) + } + success + } + return proceeded + } + + private fun extractFormattingAttribute(context: Context, attrs: AttributeSet): Boolean { + val a = context.theme.obtainStyledAttributes( + attrs, R.styleable.PasteSensitiveTextInputEditText, 0, 0 + ) + return try { + a.getBoolean(R.styleable.PasteSensitiveTextInputEditText_allowFormatting, true) + } finally { + a.recycle() + } + } + + fun setFormattingAllowed(formattingAllowed: Boolean) { + this.formattingAllowed = formattingAllowed + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java deleted file mode 100644 index 21af5ee78d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.java +++ /dev/null @@ -1,36 +0,0 @@ -package fr.free.nrw.commons.ui.widget; - -import android.content.Context; -import android.text.method.LinkMovementMethod; -import android.util.AttributeSet; - -import androidx.appcompat.widget.AppCompatTextView; - -import fr.free.nrw.commons.utils.StringUtil; - -/** - * An {@link AppCompatTextView} which formats the text to HTML displayable text and makes any - * links clickable. - */ -public class HtmlTextView extends AppCompatTextView { - - /** - * Constructs a new instance of HtmlTextView - * @param context the context of the view - * @param attrs the set of attributes for the view - */ - public HtmlTextView(Context context, AttributeSet attrs) { - super(context, attrs); - - setMovementMethod(LinkMovementMethod.getInstance()); - setText(StringUtil.fromHtml(getText().toString())); - } - - /** - * Sets the text to be displayed - * @param newText the text to be displayed - */ - public void setHtmlText(String newText) { - setText(StringUtil.fromHtml(newText)); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt new file mode 100644 index 0000000000..48433136f4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/HtmlTextView.kt @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.ui.widget + +import android.content.Context +import android.text.method.LinkMovementMethod +import android.util.AttributeSet + +import androidx.appcompat.widget.AppCompatTextView + +import fr.free.nrw.commons.utils.StringUtil + +/** + * An [AppCompatTextView] which formats the text to HTML displayable text and makes any + * links clickable. + */ +class HtmlTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : AppCompatTextView(context, attrs) { + + init { + movementMethod = LinkMovementMethod.getInstance() + text = StringUtil.fromHtml(text.toString()) + } + + /** + * Sets the text to be displayed + * @param newText the text to be displayed + */ + fun setHtmlText(newText: String) { + text = StringUtil.fromHtml(newText) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java deleted file mode 100644 index f36219040c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.java +++ /dev/null @@ -1,70 +0,0 @@ -package fr.free.nrw.commons.ui.widget; - -import android.app.Dialog; -import android.graphics.Color; -import android.graphics.drawable.ColorDrawable; -import android.os.Bundle; -import android.view.Gravity; -import android.view.View; -import android.view.Window; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.fragment.app.DialogFragment; - -/** - * a formatted dialog fragment - * This class is used by NearbyInfoDialog - */ -public abstract class OverlayDialog extends DialogFragment { - - /** - * creates a DialogFragment with the correct style and theme - * @param savedInstanceState bundle re-constructed from a previous saved state - */ - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light); - } - - /** - * When the view is created, sets the dialog layout to full screen - * - * @param view the view being used - * @param savedInstanceState bundle re-constructed from a previous saved state - */ - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - setDialogLayoutToFullScreen(); - super.onViewCreated(view, savedInstanceState); - } - - /** - * sets the dialog layout to fullscreen - */ - private void setDialogLayoutToFullScreen() { - Window window = getDialog().getWindow(); - WindowManager.LayoutParams wlp = window.getAttributes(); - window.requestFeature(Window.FEATURE_NO_TITLE); - wlp.gravity = Gravity.BOTTOM; - wlp.width = WindowManager.LayoutParams.MATCH_PARENT; - wlp.height = WindowManager.LayoutParams.MATCH_PARENT; - window.setAttributes(wlp); - } - - /** - * builds custom dialog container - * - * @param savedInstanceState the previously saved state - * @return the dialog - */ - @NonNull - @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - Dialog dialog = super.onCreateDialog(savedInstanceState); - Window window = dialog.getWindow(); - window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); - return dialog; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt new file mode 100644 index 0000000000..5d4bc2b469 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/widget/OverlayDialog.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.ui.widget + +import android.app.Dialog +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.Gravity +import android.view.View +import android.view.Window +import android.view.WindowManager + +import androidx.fragment.app.DialogFragment + +/** + * A formatted dialog fragment + * This class is used by NearbyInfoDialog + */ +abstract class OverlayDialog : DialogFragment() { + + /** + * Creates a DialogFragment with the correct style and theme + * @param savedInstanceState bundle re-constructed from a previous saved state + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_FRAME, android.R.style.Theme_Holo_Light) + } + + /** + * When the view is created, sets the dialog layout to full screen + * + * @param view the view being used + * @param savedInstanceState bundle re-constructed from a previous saved state + */ + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + setDialogLayoutToFullScreen() + super.onViewCreated(view, savedInstanceState) + } + + /** + * Sets the dialog layout to fullscreen + */ + private fun setDialogLayoutToFullScreen() { + val window = dialog?.window ?: return + val wlp = window.attributes + window.requestFeature(Window.FEATURE_NO_TITLE) + wlp.gravity = Gravity.BOTTOM + wlp.width = WindowManager.LayoutParams.MATCH_PARENT + wlp.height = WindowManager.LayoutParams.MATCH_PARENT + window.attributes = wlp + } + + /** + * Builds custom dialog container + * + * @param savedInstanceState the previously saved state + * @return the dialog + */ + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val dialog = super.onCreateDialog(savedInstanceState) + dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + return dialog + } +} From 5f1d284309bc4bc46913cb4a2d418618c01a5bf4 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 21 Nov 2024 13:01:41 +0100 Subject: [PATCH 027/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-br/strings.xml | 32 ++++++++++++++++++++++++-- app/src/main/res/values-de/strings.xml | 2 +- app/src/main/res/values-sv/strings.xml | 10 ++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 1c7d09617a..5d86b1fcf8 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -53,6 +53,13 @@ gant an aotre-implijout %1$s e vo ar skeudenn-mañ gant an aotreoù-implijout %1$s e vo ar skeudenn-mañ + + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + Ergerzhout Neuz Hollek @@ -73,7 +80,7 @@ Kevreet oc\'h ! N\'haller ket kevreañ! N\'eo ket bet kavet ar restr. Klask gant unan all. - Dilesadur c\'hwitet, kevreit en-dro mar plij + Dilesadur c\'hwitet. Kevreit en-dro mar plij. Kroget da enporzhiañ! %1$s bet pellgaset ! Pouezit evit gwelet hoc\'h enporzhiadenn @@ -179,6 +186,7 @@ Aotre ret ; skrivañ war al lec\'h stokañ diavaez. Ne c\'hall ket an arload tizhout ho kamera hep an dra-se. Mat eo Diwallit + Anv ar restr doubl kavet Enporzhiañ Ya Ket @@ -253,6 +261,7 @@ Commons Ho priziadur FAG + Sturlevr an implijer Lezel an tutorial a-gostez Kenrouedad dihegerz Fazi war enklask kemennoù @@ -293,6 +302,7 @@ Respont fall Rannañ an arloadoù C\'hwelañ + Skeudenn ebet en takad-mañ Klask nevez ebet Istor klask diverket Dilemel @@ -308,6 +318,7 @@ Skeudennoù implijet Ur fazi zo bet ! Implijit un anv aozer personelaet + Anv aozer personelaet Degasadennoù Nepell Kemennoù @@ -329,12 +340,14 @@ Nullañ an enporzhiadur Kenderc\'hel an enporzhiadur (Evit holl skeudennoù an hollad) + Furchal en takad-mañ Goulenn aotre Arabat goulenn en-dro Aotren Disteurel Graet Trugarekaet eo bet %1$s gant berzh + Ur fazi zo bet en ur drugarekaat %1$s Trugarekaat %1$s Skeudenn da-heul Ya, perak pas @@ -352,6 +365,7 @@ Lec\'hiadur Doare kamera Meziant + Rannañ an arload gant... Titouroù ar skeudenn Rummad ebet kavet Deskrivadur ebet kavet @@ -365,13 +379,18 @@ Berzh Hizivaat ar rummadoù Berzh + Ouzhpennet eo bet an daveennoù %1$s. Kont krouet! Testenn eilet er golver Ur fazi zo bet! Bez\' ez eus anezhañ + Ur skeudenn zo ezhomm + Seurt lec\'h: + Pont, mirdi, leti h.a. MEDIA KLASOÙ BUGALE KLASOÙ KERENT + Ur skeudenn eus %1$s eo? Sinedoù Arventennoù Lamet eus ar sinedoù @@ -400,8 +419,10 @@ Enporzhiañ Nepell Implijet + Ma renk Skeudennoù a-zoare Nullañ an enporzhiadur + Taolenniñ a ra Yezh etrefas an arload Lenn muioc\'h En holl yezhoù @@ -416,7 +437,7 @@ GOUZOUT HIROC\'H Degasadennoù an implijer: %s Taolioù-kaer an implijer: %s - Gwelet pajenn an implijer + Gwelet profil an implijer Kemmañ ar rummadoù Dibarzhioù araokaet Lec\'hiadur ebet kavet @@ -432,4 +453,11 @@ Lemel al lec\'hiadur Lec\'hiadur lamet! Trugarekaat an aozer + Kaozeadenn + C\'hwitet + Dilemel + Nullañ + Restr %1$s dilamet gant berzh + Ur fazi zo bet en ur zilemel ar restr %1$s + Al lec\'h-mañ en deus ur skeudenn dija. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 215ecc9855..c19772f4dd 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -820,7 +820,7 @@ Ortsdaten konnten nicht geladen werden Ordner löschen Löschung bestätigen - Bist du sicher, dass du den Ordner %1$s löschen möchten, die %2$d Datenobjekte enthalten? + Bist du sicher, dass du den Ordner %1$s löschen möchten, der %2$d Datenobjekte enthält? Löschen Abbrechen Ordner %1$s erfolgreich gelöscht diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 646fb34c0e..5ffde0b598 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -125,6 +125,7 @@ Sök kategorier Sök efter objekt som din mediafil skildrar (berg, Taj Mahal, etc.) Spara + Överflödesmeny Uppdatera Lista (Inga uppladdningar ännu) @@ -789,6 +790,15 @@ Pågår Misslyckades Kunde inte läsa in platsdata + Radera mapp + Bekräfta radering + Vill du verkligen radera mappen %1$s som innehåller %2$d objekt? + Radera + Avbryt + Mappen %1$s har raderats + Kunde inte radera mappen %1$s + Fel vid kassering av mappinnehållet: %1$s + Kunde inte hämta mappsökväg för bucket-ID: %1$d Det här platsen har ännu ingen bild. Gå och ta en! Det här platsen har redan en bild. Kollar nu om den här platsen har en bild. From cf88f9b796c23b460ce46ba4c42e3266b35329a9 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Thu, 21 Nov 2024 17:46:42 +0530 Subject: [PATCH 028/231] Migrated settings modules from Java to Kotlin (#5944) * Rename .java to .kt * Migrated settings module to Kotlin --- .../fr/free/nrw/commons/settings/Prefs.java | 21 - .../fr/free/nrw/commons/settings/Prefs.kt | 21 + .../commons/settings/SettingsActivity.java | 69 --- .../nrw/commons/settings/SettingsActivity.kt | 63 ++ .../commons/settings/SettingsFragment.java | 575 ------------------ .../nrw/commons/settings/SettingsFragment.kt | 556 +++++++++++++++++ 6 files changed, 640 insertions(+), 665 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/settings/Prefs.java create mode 100644 app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java deleted file mode 100644 index 3342143479..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.java +++ /dev/null @@ -1,21 +0,0 @@ -package fr.free.nrw.commons.settings; - -public class Prefs { - public static String GLOBAL_PREFS = "fr.free.nrw.commons.preferences"; - - public static String TRACKING_ENABLED = "eventLogging"; - public static final String DEFAULT_LICENSE = "defaultLicense"; - public static final String UPLOADS_SHOWING = "uploadsshowing"; - public static final String MANAGED_EXIF_TAGS = "managed_exif_tags"; - public static final String DESCRIPTION_LANGUAGE = "languageDescription"; - public static final String APP_UI_LANGUAGE = "appUiLanguage"; - public static final String KEY_THEME_VALUE = "appThemePref"; - - public static class Licenses { - public static final String CC_BY_SA_3 = "CC BY-SA 3.0"; - public static final String CC_BY_3 = "CC BY 3.0"; - public static final String CC_BY_SA_4 = "CC BY-SA 4.0"; - public static final String CC_BY_4 = "CC BY 4.0"; - public static final String CC0 = "CC0"; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt new file mode 100644 index 0000000000..13e8efb579 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.settings + +object Prefs { + const val GLOBAL_PREFS = "fr.free.nrw.commons.preferences" + + const val TRACKING_ENABLED = "eventLogging" + const val DEFAULT_LICENSE = "defaultLicense" + const val UPLOADS_SHOWING = "uploadsShowing" + const val MANAGED_EXIF_TAGS = "managed_exif_tags" + const val DESCRIPTION_LANGUAGE = "languageDescription" + const val APP_UI_LANGUAGE = "appUiLanguage" + const val KEY_THEME_VALUE = "appThemePref" + + object Licenses { + const val CC_BY_SA_3 = "CC BY-SA 3.0" + const val CC_BY_3 = "CC BY 3.0" + const val CC_BY_SA_4 = "CC BY-SA 4.0" + const val CC_BY_4 = "CC BY 4.0" + const val CC0 = "CC0" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java deleted file mode 100644 index ff5024b321..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.java +++ /dev/null @@ -1,69 +0,0 @@ -package fr.free.nrw.commons.settings; - -import android.os.Bundle; -import android.view.MenuItem; - -import android.view.View; -import androidx.appcompat.app.AppCompatDelegate; - -import fr.free.nrw.commons.databinding.ActivitySettingsBinding; -import fr.free.nrw.commons.theme.BaseActivity; - -/** - * allows the user to change the settings - */ -public class SettingsActivity extends BaseActivity { - - private ActivitySettingsBinding binding; -// private AppCompatDelegate settingsDelegate; - /** - * to be called when the activity starts - * @param savedInstanceState the previously saved state - */ - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivitySettingsBinding.inflate(getLayoutInflater()); - final View view = binding.getRoot(); - setContentView(view); - - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - // Get an action bar - /** - * takes care of actions taken after the creation has happened - * @param savedInstanceState the saved state - */ - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); -// if (settingsDelegate == null) { -// settingsDelegate = AppCompatDelegate.create(this, null); -// } -// settingsDelegate.onPostCreate(savedInstanceState); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * Handle action-bar clicks - * @param item the selected item - * @return true on success, false on failure - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - default: - return super.onOptionsItemSelected(item); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt new file mode 100644 index 0000000000..da79244bce --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt @@ -0,0 +1,63 @@ +package fr.free.nrw.commons.settings + +import android.os.Bundle +import android.view.MenuItem +import fr.free.nrw.commons.databinding.ActivitySettingsBinding +import fr.free.nrw.commons.theme.BaseActivity + + +/** + * allows the user to change the settings + */ +class SettingsActivity : BaseActivity() { + + private lateinit var binding: ActivitySettingsBinding +// private var settingsDelegate: AppCompatDelegate? = null + + /** + * to be called when the activity starts + * @param savedInstanceState the previously saved state + */ + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivitySettingsBinding.inflate(layoutInflater) + val view = binding.root + setContentView(view) + + setSupportActionBar(binding.toolbarBinding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + // Get an action bar + /** + * takes care of actions taken after the creation has happened + * @param savedInstanceState the saved state + */ + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) +// if (settingsDelegate == null) { +// settingsDelegate = AppCompatDelegate.create(this, null) +// } +// settingsDelegate?.onPostCreate(savedInstanceState) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + /** + * Handle action-bar clicks + * @param item the selected item + * @return true on success, false on failure + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + android.R.id.home -> { + finish() + true + } + else -> super.onOptionsItemSelected(item) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java deleted file mode 100644 index d4ed379f09..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ /dev/null @@ -1,575 +0,0 @@ -package fr.free.nrw.commons.settings; - -import static android.content.Context.MODE_PRIVATE; - -import android.Manifest.permission; -import android.app.Activity; -import android.app.Dialog; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.res.Configuration; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.View; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.EditText; -import android.widget.ListView; -import android.widget.TextView; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult; -import androidx.preference.ListPreference; -import androidx.preference.MultiSelectListPreference; -import androidx.preference.Preference; -import androidx.preference.Preference.OnPreferenceClickListener; -import androidx.preference.PreferenceFragmentCompat; -import androidx.preference.PreferenceGroupAdapter; -import androidx.preference.PreferenceScreen; -import androidx.preference.PreferenceViewHolder; -import androidx.recyclerview.widget.RecyclerView.Adapter; -import com.karumi.dexter.Dexter; -import com.karumi.dexter.MultiplePermissionsReport; -import com.karumi.dexter.PermissionToken; -import com.karumi.dexter.listener.PermissionRequest; -import com.karumi.dexter.listener.multi.MultiplePermissionsListener; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.campaigns.CampaignView; -import fr.free.nrw.commons.contributions.ContributionController; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.recentlanguages.Language; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao; -import fr.free.nrw.commons.upload.LanguagesAdapter; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.PermissionUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import javax.inject.Inject; -import javax.inject.Named; -import timber.log.Timber; - -public class SettingsFragment extends PreferenceFragmentCompat { - - @Inject - @Named("default_preferences") - JsonKvStore defaultKvStore; - - @Inject - CommonsLogSender commonsLogSender; - - @Inject - RecentLanguagesDao recentLanguagesDao; - - @Inject - ContributionController contributionController; - - @Inject - LocationServiceManager locationManager; - - private ListPreference themeListPreference; - private Preference descriptionLanguageListPreference; - private Preference appUiLanguageListPreference; - private Preference showDeletionButtonPreference; - private String keyLanguageListPreference; - private TextView recentLanguagesTextView; - private View separator; - private ListView languageHistoryListView; - private static final String GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"; - - private final ActivityResultLauncher cameraPickLauncherForResult = - registerForActivityResult(new StartActivityForResult(), - result -> { - contributionController.handleActivityResultWithCallback(requireActivity(), callbacks -> { - contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks); - }); - }); - - private ActivityResultLauncher inAppCameraLocationPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { - @Override - public void onActivityResult(Map result) { - boolean areAllGranted = true; - for (final boolean b : result.values()) { - areAllGranted = areAllGranted && b; - } - if (!areAllGranted && shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION)) { - contributionController.handleShowRationaleFlowCameraLocation(getActivity(), inAppCameraLocationPermissionLauncher, cameraPickLauncherForResult); - } - } - }); - - @Override - public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { - ApplicationlessInjection - .getInstance(getActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - - // Set the preferences from an XML resource - setPreferencesFromResource(R.xml.preferences, rootKey); - - themeListPreference = findPreference(Prefs.KEY_THEME_VALUE); - prepareTheme(); - - MultiSelectListPreference multiSelectListPref = findPreference(Prefs.MANAGED_EXIF_TAGS); - if (multiSelectListPref != null) { - multiSelectListPref.setOnPreferenceChangeListener((preference, newValue) -> { - if (newValue instanceof HashSet && !((HashSet) newValue).contains(getString(R.string.exif_tag_location))) { - defaultKvStore.putBoolean("has_user_manually_removed_location", true); - } - return true; - }); - } - - Preference inAppCameraLocationPref = findPreference("inAppCameraLocationPref"); - - inAppCameraLocationPref.setOnPreferenceChangeListener( - (preference, newValue) -> { - boolean isInAppCameraLocationTurnedOn = (boolean) newValue; - if (isInAppCameraLocationTurnedOn) { - createDialogsAndHandleLocationPermissions(getActivity()); - } - return true; - } - ); - - // Gets current language code from shared preferences - String languageCode; - - appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref"); - assert appUiLanguageListPreference != null; - keyLanguageListPreference = appUiLanguageListPreference.getKey(); - languageCode = getCurrentLanguageCode(keyLanguageListPreference); - assert languageCode != null; - if (languageCode.equals("")) { - // If current language code is empty, means none selected by user yet so use phone local - appUiLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage()); - } else { - // If any language is selected by user previously, use it - Locale defLocale = createLocale(languageCode); - appUiLanguageListPreference.setSummary((defLocale).getDisplayLanguage(defLocale)); - } - appUiLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - prepareAppLanguages(appUiLanguageListPreference.getKey()); - return true; - } - }); - - descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref"); - assert descriptionLanguageListPreference != null; - keyLanguageListPreference = descriptionLanguageListPreference.getKey(); - languageCode = getCurrentLanguageCode(keyLanguageListPreference); - assert languageCode != null; - if (languageCode.equals("")) { - // If current language code is empty, means none selected by user yet so use phone local - descriptionLanguageListPreference.setSummary(Locale.getDefault().getDisplayLanguage()); - } else { - // If any language is selected by user previously, use it - Locale defLocale = createLocale(languageCode); - descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } - descriptionLanguageListPreference.setOnPreferenceClickListener(new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - prepareAppLanguages(descriptionLanguageListPreference.getKey()); - return true; - } - }); - - // - showDeletionButtonPreference = findPreference("displayDeletionButton"); - if (showDeletionButtonPreference != null) { - showDeletionButtonPreference.setOnPreferenceChangeListener((preference, newValue) -> { - boolean isEnabled = (boolean) newValue; - // Save preference when user toggles the button - defaultKvStore.putBoolean("displayDeletionButton", isEnabled); - return true; - }); - } - - - Preference betaTesterPreference = findPreference("becomeBetaTester"); - betaTesterPreference.setOnPreferenceClickListener(preference -> { - Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link))); - return true; - }); - Preference sendLogsPreference = findPreference("sendLogFile"); - sendLogsPreference.setOnPreferenceClickListener(preference -> { - checkPermissionsAndSendLogs(); - return true; - }); - - Preference documentBasedPickerPreference = findPreference("openDocumentPhotoPickerPref"); - documentBasedPickerPreference.setOnPreferenceChangeListener( - (preference, newValue) -> { - boolean isGetContentPickerTurnedOn = !(boolean) newValue; - if (isGetContentPickerTurnedOn) { - showLocationLossWarning(); - } - return true; - } - ); - // Disable some settings when not logged in. - if (defaultKvStore.getBoolean("login_skipped", false)) { - findPreference("useExternalStorage").setEnabled(false); - findPreference("useAuthorName").setEnabled(false); - findPreference("displayNearbyCardView").setEnabled(false); - findPreference("descriptionDefaultLanguagePref").setEnabled(false); - findPreference("displayLocationPermissionForCardView").setEnabled(false); - findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE).setEnabled(false); - findPreference("managed_exif_tags").setEnabled(false); - findPreference("openDocumentPhotoPickerPref").setEnabled(false); - findPreference("inAppCameraLocationPref").setEnabled(false); - } - } - - /** - * Asks users to provide location access - * - * @param activity - */ - private void createDialogsAndHandleLocationPermissions(Activity activity) { - inAppCameraLocationPermissionLauncher.launch(new String[]{permission.ACCESS_FINE_LOCATION}); - } - - /** - * On some devices, the new Photo Picker with GET_CONTENT takeover - * redacts location tags from EXIF metadata - * - * Show warning to the user when ACTION_GET_CONTENT intent is enabled - */ - private void showLocationLossWarning() { - DialogUtil.showAlertDialog( - getActivity(), - null, - getString(R.string.location_loss_warning), - getString(R.string.ok), - getString(R.string.read_help_link), - () -> {}, - () -> Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)), - null, - true - ); - } - - @Override - protected Adapter onCreateAdapter(final PreferenceScreen preferenceScreen) { - return new PreferenceGroupAdapter(preferenceScreen) { - @Override - public void onBindViewHolder(PreferenceViewHolder holder, int position) { - super.onBindViewHolder(holder, position); - Preference preference = getItem(position); - View iconFrame = holder.itemView.findViewById(R.id.icon_frame); - if (iconFrame != null) { - iconFrame.setVisibility(View.GONE); - } - } - }; - } - - /** - * Sets the theme pref - */ - private void prepareTheme() { - themeListPreference.setOnPreferenceChangeListener((preference, newValue) -> { - getActivity().recreate(); - return true; - }); - } - - /** - * Prepare and Show language selection dialog box - * Uses previously saved language if there is any, if not uses phone locale as initial language. - * Disable default/already selected language from dialog box - * Get ListPreference key and act accordingly for each ListPreference. - * saves value chosen by user to shared preferences - * to remember later and recall MainActivity to reflect language changes - * @param keyListPreference - */ - private void prepareAppLanguages(final String keyListPreference) { - - // Gets current language code from shared preferences - final String languageCode = getCurrentLanguageCode(keyListPreference); - final List recentLanguages = recentLanguagesDao.getRecentLanguages(); - HashMap selectedLanguages = new HashMap<>(); - - if (keyListPreference.equals("appUiDefaultLanguagePref")) { - - assert languageCode != null; - if (languageCode.equals("")) { - selectedLanguages.put(0, Locale.getDefault().getLanguage()); - } else { - selectedLanguages.put(0, languageCode); - } - } else if (keyListPreference.equals("descriptionDefaultLanguagePref")) { - - assert languageCode != null; - if (languageCode.equals("")) { - selectedLanguages.put(0, Locale.getDefault().getLanguage()); - - } else { - selectedLanguages.put(0, languageCode); - } - } - - LanguagesAdapter languagesAdapter = new LanguagesAdapter( - getActivity(), - selectedLanguages - ); - - Dialog dialog = new Dialog(getActivity()); - dialog.setContentView(R.layout.dialog_select_language); - dialog.setCanceledOnTouchOutside(true); - dialog.getWindow().setLayout((int)(getActivity().getResources().getDisplayMetrics().widthPixels*0.90), - (int)(getActivity().getResources().getDisplayMetrics().heightPixels*0.90)); - dialog.show(); - - EditText editText = dialog.findViewById(R.id.search_language); - ListView listView = dialog.findViewById(R.id.language_list); - languageHistoryListView = dialog.findViewById(R.id.language_history_list); - recentLanguagesTextView = dialog.findViewById(R.id.recent_searches); - separator = dialog.findViewById(R.id.separator); - - setUpRecentLanguagesSection(recentLanguages, selectedLanguages); - - listView.setAdapter(languagesAdapter); - - editText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence charSequence, int i, int i1, - int i2) { - hideRecentLanguagesSection(); - } - - @Override - public void onTextChanged(CharSequence charSequence, int i, int i1, - int i2) { - languagesAdapter.getFilter().filter(charSequence); - } - - @Override - public void afterTextChanged(Editable editable) { - - } - }); - - languageHistoryListView.setOnItemClickListener((adapterView, view, position, id) -> { - onRecentLanguageClicked(keyListPreference, dialog, adapterView, position); - }); - - listView.setOnItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView adapterView, View view, int i, - long l) { - String languageCode = ((LanguagesAdapter) adapterView.getAdapter()) - .getLanguageCode(i); - final String languageName = ((LanguagesAdapter) adapterView.getAdapter()) - .getLanguageName(i); - final boolean isExists = recentLanguagesDao.findRecentLanguage(languageCode); - if (isExists) { - recentLanguagesDao.deleteRecentLanguage(languageCode); - } - recentLanguagesDao.addRecentLanguage(new Language(languageName, languageCode)); - saveLanguageValue(languageCode, keyListPreference); - Locale defLocale = createLocale(languageCode); - if(keyListPreference.equals("appUiDefaultLanguagePref")) { - appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - setLocale(requireActivity(), languageCode); - getActivity().recreate(); - final Intent intent = new Intent(getActivity(), MainActivity.class); - startActivity(intent); - }else { - descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } - dialog.dismiss(); - } - }); - - dialog.setOnDismissListener( - dialogInterface -> languagesAdapter.getFilter().filter("")); - } - - /** - * Set up recent languages section - * - * @param recentLanguages recently used languages - * @param selectedLanguages selected languages - */ - private void setUpRecentLanguagesSection(List recentLanguages, - HashMap selectedLanguages) { - if (recentLanguages.isEmpty()) { - languageHistoryListView.setVisibility(View.GONE); - recentLanguagesTextView.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } else { - if (recentLanguages.size() > 5) { - for (int i = recentLanguages.size()-1; i >=5; i--) { - recentLanguagesDao - .deleteRecentLanguage(recentLanguages.get(i).getLanguageCode()); - } - } - languageHistoryListView.setVisibility(View.VISIBLE); - recentLanguagesTextView.setVisibility(View.VISIBLE); - separator.setVisibility(View.VISIBLE); - final RecentLanguagesAdapter recentLanguagesAdapter - = new RecentLanguagesAdapter( - getActivity(), - recentLanguagesDao.getRecentLanguages(), - selectedLanguages); - languageHistoryListView.setAdapter(recentLanguagesAdapter); - } - } - - /** - * Handles click event for recent language section - */ - private void onRecentLanguageClicked(String keyListPreference, Dialog dialog, AdapterView adapterView, - int position) { - final String recentLanguageCode = ((RecentLanguagesAdapter) adapterView.getAdapter()) - .getLanguageCode(position); - final String recentLanguageName = ((RecentLanguagesAdapter) adapterView.getAdapter()) - .getLanguageName(position); - final boolean isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode); - if (isExists) { - recentLanguagesDao.deleteRecentLanguage(recentLanguageCode); - } - recentLanguagesDao.addRecentLanguage( - new Language(recentLanguageName, recentLanguageCode)); - saveLanguageValue(recentLanguageCode, keyListPreference); - final Locale defLocale = createLocale(recentLanguageCode); - if (keyListPreference.equals("appUiDefaultLanguagePref")) { - appUiLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - setLocale(requireActivity(), recentLanguageCode); - getActivity().recreate(); - final Intent intent = new Intent(getActivity(), MainActivity.class); - startActivity(intent); - } else { - descriptionLanguageListPreference.setSummary(defLocale.getDisplayLanguage(defLocale)); - } - dialog.dismiss(); - } - - /** - * Remove the section of recent languages - */ - private void hideRecentLanguagesSection() { - languageHistoryListView.setVisibility(View.GONE); - recentLanguagesTextView.setVisibility(View.GONE); - separator.setVisibility(View.GONE); - } - - /** - * Changing the default app language with selected one and save it to SharedPreferences - */ - public void setLocale(final Activity activity, String userSelectedValue) { - if (userSelectedValue.equals("")) { - userSelectedValue = Locale.getDefault().getLanguage(); - } - final Locale locale = createLocale(userSelectedValue); - Locale.setDefault(locale); - final Configuration configuration = new Configuration(); - configuration.locale = locale; - activity.getBaseContext().getResources().updateConfiguration(configuration, - activity.getBaseContext().getResources().getDisplayMetrics()); - - final SharedPreferences.Editor editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit(); - editor.putString("language", userSelectedValue); - editor.apply(); - } - - /** - * Create Locale based on different types of language codes - * @param languageCode - * @return Locale and throws error for invalid language codes - */ - public static Locale createLocale(String languageCode) { - String[] parts = languageCode.split("-"); - switch (parts.length) { - case 1: - return new Locale(parts[0]); - case 2: - return new Locale(parts[0], parts[1]); - case 3: - return new Locale(parts[0], parts[1], parts[2]); - default: - throw new IllegalArgumentException("Invalid language code: " + languageCode); - } - } - - /** - * Save userselected language in List Preference - * @param userSelectedValue - * @param preferenceKey - */ - private void saveLanguageValue(final String userSelectedValue, final String preferenceKey) { - if (preferenceKey.equals("appUiDefaultLanguagePref")) { - defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue); - } else if (preferenceKey.equals("descriptionDefaultLanguagePref")) { - defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue); - } - } - - /** - * Gets current language code from shared preferences - * @param preferenceKey - * @return - */ - private String getCurrentLanguageCode(final String preferenceKey) { - if (preferenceKey.equals("appUiDefaultLanguagePref")) { - return defaultKvStore.getString(Prefs.APP_UI_LANGUAGE, ""); - } - if (preferenceKey.equals("descriptionDefaultLanguagePref")) { - return defaultKvStore.getString(Prefs.DESCRIPTION_LANGUAGE, ""); - } - return null; - } - - /** - * First checks for external storage permissions and then sends logs via email - */ - private void checkPermissionsAndSendLogs() { - if (PermissionUtils.hasPermission(getActivity(), PermissionUtils.getPERMISSIONS_STORAGE())) { - commonsLogSender.send(getActivity(), null); - } else { - requestExternalStoragePermissions(); - } - } - - /** - * Requests external storage permissions and shows a toast stating that log collection has - * started - */ - private void requestExternalStoragePermissions() { - Dexter.withActivity(getActivity()) - .withPermissions(PermissionUtils.getPERMISSIONS_STORAGE()) - .withListener(new MultiplePermissionsListener() { - @Override - public void onPermissionsChecked(MultiplePermissionsReport report) { - ViewUtil.showLongToast(getActivity(), - getResources().getString(R.string.log_collection_started)); - } - - @Override - public void onPermissionRationaleShouldBeShown( - List permissions, PermissionToken token) { - - } - }) - .onSameThread() - .check(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt new file mode 100644 index 0000000000..53f6b28fe2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -0,0 +1,556 @@ +package fr.free.nrw.commons.settings + +import android.Manifest.permission +import android.app.Activity +import android.app.Dialog +import android.content.Context.MODE_PRIVATE +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import android.text.Editable +import android.text.TextWatcher +import android.view.View +import android.widget.AdapterView +import android.widget.EditText +import android.widget.ListView +import android.widget.TextView +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceGroupAdapter +import androidx.preference.PreferenceScreen +import androidx.preference.PreferenceViewHolder +import androidx.recyclerview.widget.RecyclerView.Adapter +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.PermissionToken +import com.karumi.dexter.listener.PermissionRequest +import com.karumi.dexter.listener.multi.MultiplePermissionsListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.campaigns.CampaignView +import fr.free.nrw.commons.contributions.ContributionController +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.recentlanguages.Language +import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao +import fr.free.nrw.commons.upload.LanguagesAdapter +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.PermissionUtils +import fr.free.nrw.commons.utils.ViewUtil +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + +class SettingsFragment : PreferenceFragmentCompat() { + + @Inject + @field: Named("default_preferences") + lateinit var defaultKvStore: JsonKvStore + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + lateinit var recentLanguagesDao: RecentLanguagesDao + + @Inject + lateinit var contributionController: ContributionController + + @Inject + lateinit var locationManager: LocationServiceManager + + private var themeListPreference: ListPreference? = null + private var descriptionLanguageListPreference: Preference? = null + private var appUiLanguageListPreference: Preference? = null + private var showDeletionButtonPreference: Preference? = null + private var keyLanguageListPreference: String? = null + private var recentLanguagesTextView: TextView? = null + private var separator: View? = null + private var languageHistoryListView: ListView? = null + private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher> + private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content" + + private val cameraPickLauncherForResult: ActivityResultLauncher = + registerForActivityResult(StartActivityForResult()) { result -> + contributionController.handleActivityResultWithCallback(requireActivity()) { callbacks -> + contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks) + } + } + + /** + * to be called when the fragment creates preferences + * @param savedInstanceState the previously saved state + * @param rootKey the root key for preferences + */ + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + + // Set the preferences from an XML resource + setPreferencesFromResource(R.xml.preferences, rootKey) + + themeListPreference = findPreference(Prefs.KEY_THEME_VALUE) + prepareTheme() + + val multiSelectListPref: MultiSelectListPreference? = findPreference( + Prefs.MANAGED_EXIF_TAGS + ) + multiSelectListPref?.setOnPreferenceChangeListener { _, newValue -> + if (newValue is HashSet<*> && !newValue.contains(getString(R.string.exif_tag_location))) + { + defaultKvStore.putBoolean("has_user_manually_removed_location", true) + } + true + } + + val inAppCameraLocationPref: Preference? = findPreference("inAppCameraLocationPref") + inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue -> + val isInAppCameraLocationTurnedOn = newValue as Boolean + if (isInAppCameraLocationTurnedOn) { + createDialogsAndHandleLocationPermissions(requireActivity()) + } + true + } + + inAppCameraLocationPermissionLauncher = registerForActivityResult( + RequestMultiplePermissions() + ) { result -> + var areAllGranted = true + for (b in result.values) { + areAllGranted = areAllGranted && b + } + if ( + !areAllGranted + && + shouldShowRequestPermissionRationale(permission.ACCESS_FINE_LOCATION) + ) { + contributionController.handleShowRationaleFlowCameraLocation( + requireActivity(), + inAppCameraLocationPermissionLauncher, + cameraPickLauncherForResult + ) + } + } + + // Gets current language code from shared preferences + var languageCode: String? + + appUiLanguageListPreference = findPreference("appUiDefaultLanguagePref") + appUiLanguageListPreference?.let { appUiLanguageListPreference -> + keyLanguageListPreference = appUiLanguageListPreference.key + languageCode = getCurrentLanguageCode(keyLanguageListPreference!!) + + languageCode?.let { code -> + if (code.isEmpty()) { + // If current language code is empty, means none selected by user yet so use + // phone locale + appUiLanguageListPreference.summary = Locale.getDefault().displayLanguage + } else { + // If any language is selected by user previously, use it + val defLocale = createLocale(code) + appUiLanguageListPreference.summary = defLocale.getDisplayLanguage(defLocale) + } + } + + appUiLanguageListPreference.setOnPreferenceClickListener { + prepareAppLanguages(keyLanguageListPreference!!) + true + } + } + + descriptionLanguageListPreference = findPreference("descriptionDefaultLanguagePref") + descriptionLanguageListPreference?.let { descriptionLanguageListPreference -> + languageCode = getCurrentLanguageCode(descriptionLanguageListPreference.key) + + languageCode?.let { code -> + if (code.isEmpty()) { + // If current language code is empty, means none selected by user yet so use + // phone locale + descriptionLanguageListPreference.summary = Locale.getDefault().displayLanguage + } else { + // If any language is selected by user previously, use it + val defLocale = createLocale(code) + descriptionLanguageListPreference.summary = defLocale.getDisplayLanguage( + defLocale + ) + } + } + + descriptionLanguageListPreference.setOnPreferenceClickListener { + prepareAppLanguages(it.key) + true + } + } + + showDeletionButtonPreference = findPreference("displayDeletionButton") + showDeletionButtonPreference?.setOnPreferenceChangeListener { _, newValue -> + val isEnabled = newValue as Boolean + // Save preference when user toggles the button + defaultKvStore.putBoolean("displayDeletionButton", isEnabled) + true + } + + val betaTesterPreference: Preference? = findPreference("becomeBetaTester") + betaTesterPreference?.setOnPreferenceClickListener { + Utils.handleWebUrl(requireActivity(), Uri.parse(getString(R.string.beta_opt_in_link))) + true + } + + val sendLogsPreference: Preference? = findPreference("sendLogFile") + sendLogsPreference?.setOnPreferenceClickListener { + checkPermissionsAndSendLogs() + true + } + + val documentBasedPickerPreference: Preference? = findPreference( + "openDocumentPhotoPickerPref" + ) + documentBasedPickerPreference?.setOnPreferenceChangeListener { _, newValue -> + val isGetContentPickerTurnedOn = newValue as Boolean + if (!isGetContentPickerTurnedOn) { + showLocationLossWarning() + } + true + } + + // Disable some settings when not logged in. + if (defaultKvStore.getBoolean("login_skipped", false)) { + findPreference("useExternalStorage")?.isEnabled = false + findPreference("useAuthorName")?.isEnabled = false + findPreference("displayNearbyCardView")?.isEnabled = false + findPreference("descriptionDefaultLanguagePref")?.isEnabled = false + findPreference("displayLocationPermissionForCardView")?.isEnabled = false + findPreference(CampaignView.CAMPAIGNS_DEFAULT_PREFERENCE)?.isEnabled = false + findPreference("managed_exif_tags")?.isEnabled = false + findPreference("openDocumentPhotoPickerPref")?.isEnabled = false + findPreference("inAppCameraLocationPref")?.isEnabled = false + } + } + + /** + * Asks users to provide location access + * + * @param activity + */ + private fun createDialogsAndHandleLocationPermissions(activity: Activity) { + inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION)) + } + + /** + * On some devices, the new Photo Picker with GET_CONTENT takeover + * redacts location tags from EXIF metadata + * + * Show warning to the user when ACTION_GET_CONTENT intent is enabled + */ + private fun showLocationLossWarning() { + DialogUtil.showAlertDialog( + requireActivity(), + null, + getString(R.string.location_loss_warning), + getString(R.string.ok), + getString(R.string.read_help_link), + { }, + { Utils.handleWebUrl(requireContext(), Uri.parse(GET_CONTENT_PICKER_HELP_URL)) }, + null, + true + ) + } + + override fun onCreateAdapter(preferenceScreen: PreferenceScreen): Adapter + { + return object : PreferenceGroupAdapter(preferenceScreen) { + override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) { + super.onBindViewHolder(holder, position) + val preference = getItem(position) + val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame) + iconFrame?.visibility = View.GONE + } + } + } + + /** + * Sets the theme pref + */ + private fun prepareTheme() { + themeListPreference?.setOnPreferenceChangeListener { _, _ -> + requireActivity().recreate() + true + } + } + + /** + * Prepare and Show language selection dialog box + * Uses previously saved language if there is any, if not uses phone locale as initial language. + * Disable default/already selected language from dialog box + * Get ListPreference key and act accordingly for each ListPreference. + * saves value chosen by user to shared preferences + * to remember later and recall MainActivity to reflect language changes + * @param keyListPreference + */ + private fun prepareAppLanguages(keyListPreference: String) { + // Gets current language code from shared preferences + val languageCode = getCurrentLanguageCode(keyListPreference) + val recentLanguages = recentLanguagesDao.getRecentLanguages() + val selectedLanguages = hashMapOf() + + if (keyListPreference == "appUiDefaultLanguagePref") { + if (languageCode.isNullOrEmpty()) { + selectedLanguages[0] = Locale.getDefault().language + } else { + selectedLanguages[0] = languageCode + } + } else if (keyListPreference == "descriptionDefaultLanguagePref") { + if (languageCode.isNullOrEmpty()) { + selectedLanguages[0] = Locale.getDefault().language + } else { + selectedLanguages[0] = languageCode + } + } + + val languagesAdapter = LanguagesAdapter(requireActivity(), selectedLanguages) + + val dialog = Dialog(requireActivity()) + dialog.setContentView(R.layout.dialog_select_language) + dialog.setCanceledOnTouchOutside(true) + dialog.window?.setLayout( + (resources.displayMetrics.widthPixels * 0.90).toInt(), + (resources.displayMetrics.heightPixels * 0.90).toInt() + ) + dialog.show() + + val editText: EditText = dialog.findViewById(R.id.search_language) + val listView: ListView = dialog.findViewById(R.id.language_list) + languageHistoryListView = dialog.findViewById(R.id.language_history_list) + recentLanguagesTextView = dialog.findViewById(R.id.recent_searches) + separator = dialog.findViewById(R.id.separator) + + setUpRecentLanguagesSection(recentLanguages, selectedLanguages) + + listView.adapter = languagesAdapter + + editText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(charSequence: CharSequence, start: Int, count: Int, after: Int) { + hideRecentLanguagesSection() + } + + override fun onTextChanged(charSequence: CharSequence, start: Int, before: Int, count: Int) { + languagesAdapter.filter.filter(charSequence) + } + + override fun afterTextChanged(editable: Editable?) {} + }) + + languageHistoryListView?.setOnItemClickListener { adapterView, _, position, _ -> + onRecentLanguageClicked(keyListPreference, dialog, adapterView, position) + } + + listView.setOnItemClickListener { adapterView, _, position, _ -> + val lCode = (adapterView.adapter as LanguagesAdapter).getLanguageCode(position) + val languageName = (adapterView.adapter as LanguagesAdapter).getLanguageName(position) + val isExists = recentLanguagesDao.findRecentLanguage(lCode) + if (isExists) { + recentLanguagesDao.deleteRecentLanguage(lCode) + } + recentLanguagesDao.addRecentLanguage(Language(languageName, lCode)) + saveLanguageValue(lCode, keyListPreference) + val defLocale = createLocale(lCode) + if (keyListPreference == "appUiDefaultLanguagePref") { + appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + setLocale(requireActivity(), lCode) + requireActivity().recreate() + val intent = Intent(requireActivity(), MainActivity::class.java) + startActivity(intent) + } else { + descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + } + dialog.dismiss() + } + + dialog.setOnDismissListener { languagesAdapter.filter.filter("") } + } + + /** + * Set up recent languages section + * + * @param recentLanguages recently used languages + * @param selectedLanguages selected languages + */ + private fun setUpRecentLanguagesSection( + recentLanguages: List, + selectedLanguages: HashMap + ) { + if (recentLanguages.isEmpty()) { + languageHistoryListView?.visibility = View.GONE + recentLanguagesTextView?.visibility = View.GONE + separator?.visibility = View.GONE + } else { + if (recentLanguages.size > 5) { + for (i in recentLanguages.size - 1 downTo 5) { + recentLanguagesDao.deleteRecentLanguage(recentLanguages[i].languageCode) + } + } + languageHistoryListView?.visibility = View.VISIBLE + recentLanguagesTextView?.visibility = View.VISIBLE + separator?.visibility = View.VISIBLE + val recentLanguagesAdapter = RecentLanguagesAdapter( + requireActivity(), + recentLanguagesDao.getRecentLanguages(), + selectedLanguages + ) + languageHistoryListView?.adapter = recentLanguagesAdapter + } + } + + /** + * Handles click event for recent language section + */ + private fun onRecentLanguageClicked( + keyListPreference: String, + dialog: Dialog, + adapterView: AdapterView<*>, + position: Int + ) { + val recentLanguageCode = (adapterView.adapter as RecentLanguagesAdapter).getLanguageCode(position) + val recentLanguageName = (adapterView.adapter as RecentLanguagesAdapter).getLanguageName(position) + val isExists = recentLanguagesDao.findRecentLanguage(recentLanguageCode) + if (isExists) { + recentLanguagesDao.deleteRecentLanguage(recentLanguageCode) + } + recentLanguagesDao.addRecentLanguage(Language(recentLanguageName, recentLanguageCode)) + saveLanguageValue(recentLanguageCode, keyListPreference) + val defLocale = createLocale(recentLanguageCode) + if (keyListPreference == "appUiDefaultLanguagePref") { + appUiLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + setLocale(requireActivity(), recentLanguageCode) + requireActivity().recreate() + val intent = Intent(requireActivity(), MainActivity::class.java) + startActivity(intent) + } else { + descriptionLanguageListPreference?.summary = defLocale.getDisplayLanguage(defLocale) + } + dialog.dismiss() + } + + /** + * Remove the section of recent languages + */ + private fun hideRecentLanguagesSection() { + languageHistoryListView?.visibility = View.GONE + recentLanguagesTextView?.visibility = View.GONE + separator?.visibility = View.GONE + } + + /** + * Changing the default app language with selected one and save it to SharedPreferences + */ + fun setLocale(activity: Activity, userSelectedValue: String) { + var selectedLanguage = userSelectedValue + if (selectedLanguage == "") { + selectedLanguage = Locale.getDefault().language + } + val locale = createLocale(selectedLanguage) + Locale.setDefault(locale) + val configuration = Configuration() + configuration.locale = locale + activity.baseContext.resources.updateConfiguration(configuration, activity.baseContext.resources.displayMetrics) + + val editor = activity.getSharedPreferences("Settings", MODE_PRIVATE).edit() + editor.putString("language", selectedLanguage) + editor.apply() + } + + /** + * Create Locale based on different types of language codes + * @param languageCode + * @return Locale and throws error for invalid language codes + */ + fun createLocale(languageCode: String): Locale { + val parts = languageCode.split("-") + return when (parts.size) { + 1 -> Locale(parts[0]) + 2 -> Locale(parts[0], parts[1]) + 3 -> Locale(parts[0], parts[1], parts[2]) + else -> throw IllegalArgumentException("Invalid language code: $languageCode") + } + } + + /** + * Save userSelected language in List Preference + * @param userSelectedValue + * @param preferenceKey + */ + private fun saveLanguageValue(userSelectedValue: String, preferenceKey: String) { + when (preferenceKey) { + "appUiDefaultLanguagePref" -> defaultKvStore.putString(Prefs.APP_UI_LANGUAGE, userSelectedValue) + "descriptionDefaultLanguagePref" -> defaultKvStore.putString(Prefs.DESCRIPTION_LANGUAGE, userSelectedValue) + } + } + + /** + * Gets current language code from shared preferences + * @param preferenceKey + * @return + */ + private fun getCurrentLanguageCode(preferenceKey: String): String? { + return when (preferenceKey) { + "appUiDefaultLanguagePref" -> defaultKvStore.getString( + Prefs.APP_UI_LANGUAGE, "" + ) + "descriptionDefaultLanguagePref" -> defaultKvStore.getString( + Prefs.DESCRIPTION_LANGUAGE, "" + ) + else -> null + } + } + + /** + * First checks for external storage permissions and then sends logs via email + */ + private fun checkPermissionsAndSendLogs() { + if ( + PermissionUtils.hasPermission( + requireActivity(), + PermissionUtils.PERMISSIONS_STORAGE + ) + ) { + commonsLogSender.send(requireActivity(), null) + } else { + requestExternalStoragePermissions() + } + } + + /** + * Requests external storage permissions and shows a toast stating that log collection has + * started + */ + private fun requestExternalStoragePermissions() { + Dexter.withActivity(requireActivity()) + .withPermissions(*PermissionUtils.PERMISSIONS_STORAGE) + .withListener(object : MultiplePermissionsListener { + override fun onPermissionsChecked(report: MultiplePermissionsReport) { + ViewUtil.showLongToast(requireActivity(), getString(R.string.log_collection_started)) + } + + override fun onPermissionRationaleShouldBeShown( + permissions: List, token: PermissionToken + ) { + // No action needed + } + }) + .onSameThread() + .check() + } + +} From 088dd2479e56523591085d5bb0a076c2903d1813 Mon Sep 17 00:00:00 2001 From: u7479759 Date: Thu, 21 Nov 2024 23:42:54 +1100 Subject: [PATCH 029/231] Changed to data classes, and added immutability (#5905) Co-authored-by: Jinniu Du <127721018+Donutcheese@users.noreply.github.com> --- .../profile/achievements/Achievements.kt | 121 ++++------------ .../achievements/AchievementsFragment.java | 134 ++++++++++-------- .../profile/achievements/FeaturedImages.kt | 2 +- 3 files changed, 104 insertions(+), 153 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt index 861040fcff..7b23db2cd9 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/Achievements.kt @@ -1,104 +1,45 @@ package fr.free.nrw.commons.profile.achievements /** - * Represents Achievements class and stores all the parameters + * Represents Achievements data class and stores all the parameters. + * Immutable version with default values for optional properties. */ -class Achievements { +data class Achievements( + val uniqueUsedImages: Int = 0, + val articlesUsingImages: Int = 0, + val thanksReceived: Int = 0, + val featuredImages: Int = 0, + val qualityImages: Int = 0, + val imagesUploaded: Int = 0, + val revertCount: Int = 0 +) { /** - * The count of unique images used by the wiki. - * @return The count of unique images used. - * @param uniqueUsedImages The count to set for unique images used. - */ - var uniqueUsedImages = 0 - private var articlesUsingImages = 0 - - /** - * The count of thanks received. - * @return The count of thanks received. - * @param thanksReceived The count to set for thanks received. - */ - var thanksReceived = 0 - - /** - * The count of featured images. - * @return The count of featured images. - * @param featuredImages The count to set for featured images. - */ - var featuredImages = 0 - - /** - * The count of quality images. - * @return The count of quality images. - * @param qualityImages The count to set for quality images. - */ - var qualityImages = 0 - - /** - * The count of images uploaded. - * @return The count of images uploaded. - * @param imagesUploaded The count to set for images uploaded. - */ - var imagesUploaded = 0 - private var revertCount = 0 - - constructor() {} - - /** - * constructor for achievements class to set its data members - * @param uniqueUsedImages - * @param articlesUsingImages - * @param thanksReceived - * @param featuredImages - * @param imagesUploaded - * @param revertCount - */ - constructor( - uniqueUsedImages: Int, - articlesUsingImages: Int, - thanksReceived: Int, - featuredImages: Int, - qualityImages: Int, - imagesUploaded: Int, - revertCount: Int, - ) { - this.uniqueUsedImages = uniqueUsedImages - this.articlesUsingImages = articlesUsingImages - this.thanksReceived = thanksReceived - this.featuredImages = featuredImages - this.qualityImages = qualityImages - this.imagesUploaded = imagesUploaded - this.revertCount = revertCount - } - - /** - * used to calculate the percentages of images that haven't been reverted - * @return + * Used to calculate the percentages of images that haven't been reverted. + * Returns 100 if imagesUploaded is 0 to avoid division by zero. */ val notRevertPercentage: Int - get() = - try { - (imagesUploaded - revertCount) * 100 / imagesUploaded - } catch (divideByZero: ArithmeticException) { - 100 - } + get() = if (imagesUploaded > 0) { + (imagesUploaded - revertCount) * 100 / imagesUploaded + } else { + 100 + } companion object { /** - * Get Achievements object from FeedbackResponse + * Get Achievements object from FeedbackResponse. * - * @param response - * @return + * @param response The feedback response to convert. + * @return An Achievements object with values from the response. */ @JvmStatic - fun from(response: FeedbackResponse): Achievements = - Achievements( - response.uniqueUsedImages, - response.articlesUsingImages, - response.thanksReceived, - response.featuredImages.featuredPicturesOnWikimediaCommons, - response.featuredImages.qualityImages, - 0, - response.deletedUploads, - ) + fun from(response: FeedbackResponse): Achievements = Achievements( + uniqueUsedImages = response.uniqueUsedImages, + articlesUsingImages = response.articlesUsingImages, + thanksReceived = response.thanksReceived, + featuredImages = response.featuredImages.featuredPicturesOnWikimediaCommons, + qualityImages = response.featuredImages.qualityImages, + imagesUploaded = 0, // Assuming imagesUploaded should be 0 + revertCount = response.deletedUploads + ) } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java index f44b7eb6d7..ef6a323b2e 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java @@ -105,7 +105,7 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle sa // Used for the setting the size of imageView at runtime ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) - binding.achievementBadgeImage.getLayoutParams(); + binding.achievementBadgeImage.getLayoutParams(); params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); binding.achievementBadgeImage.requestLayout(); @@ -186,37 +186,37 @@ private void setAchievements() { try{ compositeDisposable.add(okHttpJsonApiClient - .getAchievements(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setUploadCount(Achievements.from(response)); - } else { - Timber.d("success"); - binding.layoutImageReverts.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - // If the number of edits made by the user are more than 150,000 - // in some cases such high number of wiki edit counts cause the - // achievements calculator to fail in some cases, for more details - // refer Issue: #3295 - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - }, - t -> { - Timber.e(t, "Fetching achievements statistics failed"); - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } + .getAchievements(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + response -> { + if (response != null) { + setUploadCount(Achievements.from(response)); + } else { + Timber.d("success"); + binding.layoutImageReverts.setVisibility(View.INVISIBLE); + binding.achievementBadgeImage.setVisibility(View.INVISIBLE); + // If the number of edits made by the user are more than 150,000 + // in some cases such high number of wiki edit counts cause the + // achievements calculator to fail in some cases, for more details + // refer Issue: #3295 + if (numberOfEdits <= 150000) { + showSnackBarWithRetry(false); + } else { + showSnackBarWithRetry(true); } - )); + } + }, + t -> { + Timber.e(t, "Fetching achievements statistics failed"); + if (numberOfEdits <= 150000) { + showSnackBarWithRetry(false); + } else { + showSnackBarWithRetry(true); + } + } + )); } catch (Exception e){ Timber.d(e+"success"); @@ -233,15 +233,15 @@ private void setWikidataEditCount() { return; } compositeDisposable.add(okHttpJsonApiClient - .getWikidataEdits(userName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(edits -> { - numberOfEdits = edits; - binding.wikidataEdits.setText(String.valueOf(edits)); - }, e -> { - Timber.e("Error:" + e); - })); + .getWikidataEdits(userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(edits -> { + numberOfEdits = edits; + binding.wikidataEdits.setText(String.valueOf(edits)); + }, e -> { + Timber.e("Error:" + e); + })); } /** @@ -255,11 +255,11 @@ private void showSnackBarWithRetry(boolean tooManyAchievements) { if (tooManyAchievements) { binding.progressBar.setVisibility(View.GONE); ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); + R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); } else { binding.progressBar.setVisibility(View.GONE); ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); + R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); } } @@ -277,16 +277,16 @@ private void onError() { private void setUploadCount(Achievements achievements) { if (checkAccount()) { compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> setAchievementsUploadCount(achievements, uploadCount), - t -> { - Timber.e(t, "Fetching upload count failed"); - onError(); - } - )); + .getUploadCount(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + uploadCount -> setAchievementsUploadCount(achievements, uploadCount), + t -> { + Timber.e(t, "Fetching upload count failed"); + onError(); + } + )); } } @@ -295,8 +295,18 @@ private void setUploadCount(Achievements achievements) { * @param uploadCount */ private void setAchievementsUploadCount(Achievements achievements, int uploadCount) { - achievements.setImagesUploaded(uploadCount); - hideProgressBar(achievements); + // Create a new instance of Achievements with updated imagesUploaded + Achievements updatedAchievements = new Achievements( + achievements.getUniqueUsedImages(), + achievements.getArticlesUsingImages(), + achievements.getThanksReceived(), + achievements.getFeaturedImages(), + achievements.getQualityImages(), + uploadCount, // Update imagesUploaded with new value + achievements.getRevertCount() + ); + + hideProgressBar(updatedAchievements); } /** @@ -309,7 +319,7 @@ private void setUploadProgress(int uploadCount){ }else { binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); binding.imagesUploadedProgressbar.setProgress - (100*uploadCount/levelInfo.getMaxUploadCount()); + (100*uploadCount/levelInfo.getMaxUploadCount()); binding.tvUploadedImages.setText (uploadCount + "/" + levelInfo.getMaxUploadCount()); } @@ -318,8 +328,8 @@ private void setUploadProgress(int uploadCount){ private void setZeroAchievements() { String message = !Objects.equals(sessionManager.getUserName(), userName) ? - getString(R.string.no_achievements_yet, userName) : - getString(R.string.you_have_no_achievements_yet); + getString(R.string.no_achievements_yet, userName) : + getString(R.string.you_have_no_achievements_yet); DialogUtil.showAlertDialog(getActivity(), null, message, @@ -357,7 +367,7 @@ private void inflateAchievements(Achievements achievements) { // binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); binding.imagesUsedByWikiProgressBar.setProgress - (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); + (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/" + levelInfo.getMaxUniqueImages()); binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); @@ -366,7 +376,7 @@ private void inflateAchievements(Achievements achievements) { levelUpInfoString += " " + levelInfo.getLevelNumber(); binding.achievementLevel.setText(levelUpInfoString); binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, - new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); + new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber())); BasicKvStore store = new BasicKvStore(this.getContext(), userName); store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber())); @@ -378,8 +388,8 @@ private void inflateAchievements(Achievements achievements) { private void hideProgressBar(Achievements achievements) { if (binding.progressBar != null) { levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), - achievements.getUniqueUsedImages(), - achievements.getNotRevertPercentage()); + achievements.getUniqueUsedImages(), + achievements.getNotRevertPercentage()); inflateAchievements(achievements); setUploadProgress(achievements.getImagesUploaded()); setImageRevertPercentage(achievements.getNotRevertPercentage()); @@ -479,4 +489,4 @@ private boolean checkAccount(){ } return true; } -} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt index 4784103fdb..2a336d3496 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/FeaturedImages.kt @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName * Represents Featured Images on WikiMedia Commons platform * Used by Achievements and FeedbackResponse (objects) of the user */ -class FeaturedImages( +data class FeaturedImages( @field:SerializedName("Quality_images") val qualityImages: Int, @field:SerializedName("Featured_pictures_on_Wikimedia_Commons") val featuredPicturesOnWikimediaCommons: Int, ) From fe347c21fdede145e6a945750aa43a6de7ec24eb Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 22 Nov 2024 19:28:16 +0530 Subject: [PATCH 030/231] Migrated recentlanguages and repository module from Java to Kotlin (#5948) * Rename .java to .kt * Migrated recentlanguages module to Kotlin * Rename .java to .kt * Migrated repository module to Kotlin --- .../RecentLanguagesContentProvider.java | 122 ----- .../RecentLanguagesContentProvider.kt | 142 ++++++ .../recentlanguages/RecentLanguagesDao.java | 204 --------- .../recentlanguages/RecentLanguagesDao.kt | 216 +++++++++ .../commons/repository/UploadRepository.java | 423 ------------------ .../commons/repository/UploadRepository.kt | 410 +++++++++++++++++ .../upload/categories/CategoriesPresenter.kt | 34 +- .../upload/depicts/DepictsPresenter.kt | 24 +- .../structure/depictions/DepictModel.kt | 2 +- .../commons/upload/CategoriesPresenterTest.kt | 14 +- .../commons/upload/DepictsPresenterTest.kt | 8 +- .../nrw/commons/upload/UploadPresenterTest.kt | 4 +- .../upload/UploadRepositoryUnitTest.kt | 20 +- 13 files changed, 825 insertions(+), 798 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java create mode 100644 app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java create mode 100644 app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java deleted file mode 100644 index de94c4b090..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.java +++ /dev/null @@ -1,122 +0,0 @@ -package fr.free.nrw.commons.recentlanguages; - -import static fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME; -import static fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteQueryBuilder; -import android.net.Uri; -import android.text.TextUtils; -import androidx.annotation.NonNull; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.di.CommonsDaggerContentProvider; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * Content provider of recently used languages - */ -public class RecentLanguagesContentProvider extends CommonsDaggerContentProvider { - - private static final String BASE_PATH = "recent_languages"; - public static final Uri BASE_URI = - Uri.parse("content://" + BuildConfig.RECENT_LANGUAGE_AUTHORITY + "/" + BASE_PATH); - - - /** - * Append language code to the base uri - * @param languageCode Code of a language - */ - public static Uri uriForCode(final String languageCode) { - return Uri.parse(BASE_URI + "/" + languageCode); - } - - @Inject - DBOpenHelper dbOpenHelper; - - @Override - public String getType(@NonNull final Uri uri) { - return null; - } - - /** - * Queries the SQLite database for the recently used languages - * @param uri : contains the uri for recently used languages - * @param projection : contains the all fields of the table - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - * @param sortOrder : ascending or descending - */ - @Override - public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection, - final String[] selectionArgs, final String sortOrder) { - final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); - queryBuilder.setTables(TABLE_NAME); - final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - final Cursor cursor = queryBuilder.query(db, projection, selection, - selectionArgs, null, null, sortOrder); - cursor.setNotificationUri(getContext().getContentResolver(), uri); - return cursor; - } - - /** - * Handles the update query of local SQLite Database - * @param uri : contains the uri for recently used languages - * @param contentValues : new values to be entered to db - * @param selection : handles Where - * @param selectionArgs : the condition of Where clause - */ - @Override - public int update(@NonNull final Uri uri, final ContentValues contentValues, - final String selection, final String[] selectionArgs) { - final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - final int rowsUpdated; - if (TextUtils.isEmpty(selection)) { - final int id = Integer.parseInt(uri.getLastPathSegment()); - rowsUpdated = sqlDB.update(TABLE_NAME, - contentValues, - COLUMN_NAME + " = ?", - new String[]{String.valueOf(id)}); - } else { - throw new IllegalArgumentException( - "Parameter `selection` should be empty when updating an ID"); - } - - getContext().getContentResolver().notifyChange(uri, null); - return rowsUpdated; - } - - /** - * Handles the insertion of new recently used languages record to local SQLite Database - * @param uri : contains the uri for recently used languages - * @param contentValues : new values to be entered to db - */ - @Override - public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) { - final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); - final long id = sqlDB.insert(TABLE_NAME, null, contentValues); - getContext().getContentResolver().notifyChange(uri, null); - return Uri.parse(BASE_URI + "/" + id); - } - - /** - * Handles the deletion of new recently used languages record to local SQLite Database - * @param uri : contains the uri for recently used languages - */ - @Override - public int delete(@NonNull final Uri uri, final String s, final String[] strings) { - final int rows; - final SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); - Timber.d("Deleting recently used language %s", uri.getLastPathSegment()); - rows = db.delete( - TABLE_NAME, - "language_code = ?", - new String[]{uri.getLastPathSegment()} - ); - getContext().getContentResolver().notifyChange(uri, null); - return rows; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt new file mode 100644 index 0000000000..facc4384f8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesContentProvider.kt @@ -0,0 +1,142 @@ +package fr.free.nrw.commons.recentlanguages + + +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.database.sqlite.SQLiteQueryBuilder +import android.net.Uri +import android.text.TextUtils +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.di.CommonsDaggerContentProvider +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME +import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME +import javax.inject.Inject +import timber.log.Timber + + +/** + * Content provider of recently used languages + */ +class RecentLanguagesContentProvider : CommonsDaggerContentProvider() { + + companion object { + private const val BASE_PATH = "recent_languages" + val BASE_URI: Uri = + Uri.parse( + "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH" + ) + + /** + * Append language code to the base URI + * @param languageCode Code of a language + */ + @JvmStatic + fun uriForCode(languageCode: String): Uri { + return Uri.parse("$BASE_URI/$languageCode") + } + } + + @Inject + lateinit var dbOpenHelper: DBOpenHelper + + override fun getType(uri: Uri): String? { + return null + } + + /** + * Queries the SQLite database for the recently used languages + * @param uri : contains the URI for recently used languages + * @param projection : contains all fields of the table + * @param selection : handles WHERE + * @param selectionArgs : the condition of WHERE clause + * @param sortOrder : ascending or descending + */ + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + val queryBuilder = SQLiteQueryBuilder() + queryBuilder.tables = TABLE_NAME + val db = dbOpenHelper.readableDatabase + val cursor = queryBuilder.query( + db, + projection, + selection, + selectionArgs, + null, + null, + sortOrder + ) + cursor.setNotificationUri(context?.contentResolver, uri) + return cursor + } + + /** + * Handles the update query of local SQLite Database + * @param uri : contains the URI for recently used languages + * @param contentValues : new values to be entered to the database + * @param selection : handles WHERE + * @param selectionArgs : the condition of WHERE clause + */ + override fun update( + uri: Uri, + contentValues: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + val sqlDB = dbOpenHelper.writableDatabase + val rowsUpdated: Int + if (selection.isNullOrEmpty()) { + val id = uri.lastPathSegment?.toInt() + ?: throw IllegalArgumentException("Invalid URI: $uri") + rowsUpdated = sqlDB.update( + TABLE_NAME, + contentValues, + "$COLUMN_NAME = ?", + arrayOf(id.toString()) + ) + } else { + throw IllegalArgumentException("Parameter `selection` should be empty when updating an ID") + } + + context?.contentResolver?.notifyChange(uri, null) + return rowsUpdated + } + + /** + * Handles the insertion of new recently used languages record to local SQLite Database + * @param uri : contains the URI for recently used languages + * @param contentValues : new values to be entered to the database + */ + override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { + val sqlDB = dbOpenHelper.writableDatabase + val id = sqlDB.insert( + TABLE_NAME, + null, + contentValues + ) + context?.contentResolver?.notifyChange(uri, null) + return Uri.parse("$BASE_URI/$id") + } + + /** + * Handles the deletion of a recently used languages record from local SQLite Database + * @param uri : contains the URI for recently used languages + */ + override fun delete(uri: Uri, s: String?, strings: Array?): Int { + val db = dbOpenHelper.readableDatabase + Timber.d("Deleting recently used language %s", uri.lastPathSegment) + val rows = db.delete( + TABLE_NAME, + "language_code = ?", + arrayOf(uri.lastPathSegment) + ) + context?.contentResolver?.notifyChange(uri, null) + return rows + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java deleted file mode 100644 index cbb8c8a1cc..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.java +++ /dev/null @@ -1,204 +0,0 @@ -package fr.free.nrw.commons.recentlanguages; - -import android.annotation.SuppressLint; -import android.content.ContentProviderClient; -import android.content.ContentValues; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.os.RemoteException; -import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Provider; -import javax.inject.Singleton; - -/** - * Handles database operations for recently used languages - */ -@Singleton -public class RecentLanguagesDao { - - private final Provider clientProvider; - - @Inject - public RecentLanguagesDao - (@Named("recent_languages") final Provider clientProvider) { - this.clientProvider = clientProvider; - } - - /** - * Find all persisted recently used languages on database - * @return list of recently used languages - */ - public List getRecentLanguages() { - final List languages = new ArrayList<>(); - final ContentProviderClient db = clientProvider.get(); - try (final Cursor cursor = db.query( - RecentLanguagesContentProvider.BASE_URI, - RecentLanguagesDao.Table.ALL_FIELDS, - null, - new String[]{}, - null)) { - if(cursor != null && cursor.moveToLast()) { - do { - languages.add(fromCursor(cursor)); - } while (cursor.moveToPrevious()); - } - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - return languages; - } - - /** - * Add a Language to database - * @param language : Language to add - */ - public void addRecentLanguage(final Language language) { - final ContentProviderClient db = clientProvider.get(); - try { - db.insert(RecentLanguagesContentProvider.BASE_URI, toContentValues(language)); - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Delete a language from database - * @param languageCode : code of the Language to delete - */ - public void deleteRecentLanguage(final String languageCode) { - final ContentProviderClient db = clientProvider.get(); - try { - db.delete(RecentLanguagesContentProvider.uriForCode(languageCode), null, null); - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - } - - /** - * Find a language from database based on its name - * @param languageCode : code of the Language to find - * @return boolean : is language in database ? - */ - public boolean findRecentLanguage(final String languageCode) { - if (languageCode == null) { //Avoiding NPE's - return false; - } - final ContentProviderClient db = clientProvider.get(); - try (final Cursor cursor = db.query( - RecentLanguagesContentProvider.BASE_URI, - RecentLanguagesDao.Table.ALL_FIELDS, - Table.COLUMN_CODE + "=?", - new String[]{languageCode}, - null - )) { - if (cursor != null && cursor.moveToFirst()) { - return true; - } - } catch (final RemoteException e) { - throw new RuntimeException(e); - } finally { - db.release(); - } - return false; - } - - /** - * It creates an Recent Language object from data stored in the SQLite DB by using cursor - * @param cursor cursor - * @return Language object - */ - @NonNull - @SuppressLint("Range") - Language fromCursor(final Cursor cursor) { - // Hardcoding column positions! - final String languageName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)); - final String languageCode = cursor.getString(cursor.getColumnIndex(Table.COLUMN_CODE)); - return new Language(languageName, languageCode); - } - - /** - * Takes data from Language and create a content value object - * @param recentLanguage recently used language - * @return ContentValues - */ - private ContentValues toContentValues(final Language recentLanguage) { - final ContentValues cv = new ContentValues(); - cv.put(Table.COLUMN_NAME, recentLanguage.getLanguageName()); - cv.put(Table.COLUMN_CODE, recentLanguage.getLanguageCode()); - return cv; - } - - /** - * This class contains the database table architecture for recently used languages, - * It also contains queries and logic necessary to the create, update, delete this table. - */ - public static final class Table { - public static final String TABLE_NAME = "recent_languages"; - static final String COLUMN_NAME = "language_name"; - static final String COLUMN_CODE = "language_code"; - - // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. - public static final String[] ALL_FIELDS = { - COLUMN_NAME, - COLUMN_CODE - }; - - static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; - - static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" - + COLUMN_NAME + " STRING," - + COLUMN_CODE + " STRING PRIMARY KEY" - + ");"; - - /** - * This method creates a LanguagesTable in SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onCreate(final SQLiteDatabase db) { - db.execSQL(CREATE_TABLE_STATEMENT); - } - - /** - * This method deletes LanguagesTable from SQLiteDatabase - * @param db SQLiteDatabase - */ - public static void onDelete(final SQLiteDatabase db) { - db.execSQL(DROP_TABLE_STATEMENT); - onCreate(db); - } - - /** - * This method is called on migrating from a older version to a newer version - * @param db SQLiteDatabase - * @param from Version from which we are migrating - * @param to Version to which we are migrating - */ - public static void onUpdate(final SQLiteDatabase db, int from, final int to) { - if (from == to) { - return; - } - if (from < 19) { - // doesn't exist yet - from++; - onUpdate(db, from, to); - return; - } - if (from == 19) { - // table added in version 20 - onCreate(db); - from++; - onUpdate(db, from, to); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt new file mode 100644 index 0000000000..e97c4f8165 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt @@ -0,0 +1,216 @@ +package fr.free.nrw.commons.recentlanguages + +import android.annotation.SuppressLint +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.database.sqlite.SQLiteDatabase +import android.os.RemoteException +import java.util.ArrayList +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Provider +import javax.inject.Singleton + + +/** + * Handles database operations for recently used languages + */ +@Singleton +class RecentLanguagesDao @Inject constructor( + @Named("recent_languages") + private val clientProvider: Provider +) { + + /** + * Find all persisted recently used languages on database + * @return list of recently used languages + */ + fun getRecentLanguages(): List { + val languages = mutableListOf() + val db = clientProvider.get() + try { + db.query( + RecentLanguagesContentProvider.BASE_URI, + Table.ALL_FIELDS, + null, + arrayOf(), + null + )?.use { cursor -> + if (cursor.moveToLast()) { + do { + languages.add(fromCursor(cursor)) + } while (cursor.moveToPrevious()) + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + return languages + } + + /** + * Add a Language to database + * @param language : Language to add + */ + fun addRecentLanguage(language: Language) { + val db = clientProvider.get() + try { + db.insert( + RecentLanguagesContentProvider.BASE_URI, + toContentValues(language) + ) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Delete a language from database + * @param languageCode : code of the Language to delete + */ + fun deleteRecentLanguage(languageCode: String) { + val db = clientProvider.get() + try { + db.delete( + RecentLanguagesContentProvider.uriForCode(languageCode), + null, + null + ) + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + } + + /** + * Find a language from database based on its name + * @param languageCode : code of the Language to find + * @return boolean : is language in database ? + */ + fun findRecentLanguage(languageCode: String?): Boolean { + if (languageCode == null) { // Avoiding NPEs + return false + } + val db = clientProvider.get() + try { + db.query( + RecentLanguagesContentProvider.BASE_URI, + Table.ALL_FIELDS, + "${Table.COLUMN_CODE}=?", + arrayOf(languageCode), + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + return true + } + } + } catch (e: RemoteException) { + throw RuntimeException(e) + } finally { + db.release() + } + return false + } + + /** + * It creates an Recent Language object from data stored in the SQLite DB by using cursor + * @param cursor cursor + * @return Language object + */ + @SuppressLint("Range") + fun fromCursor(cursor: Cursor): Language { + // Hardcoding column positions! + val languageName = cursor.getString( + cursor.getColumnIndex(Table.COLUMN_NAME) + ) + val languageCode = cursor.getString( + cursor.getColumnIndex(Table.COLUMN_CODE) + ) + return Language(languageName, languageCode) + } + + /** + * Takes data from Language and create a content value object + * @param recentLanguage recently used language + * @return ContentValues + */ + private fun toContentValues(recentLanguage: Language): ContentValues { + return ContentValues().apply { + put(Table.COLUMN_NAME, recentLanguage.languageName) + put(Table.COLUMN_CODE, recentLanguage.languageCode) + } + } + + /** + * This class contains the database table architecture for recently used languages, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + object Table { + const val TABLE_NAME = "recent_languages" + const val COLUMN_NAME = "language_name" + const val COLUMN_CODE = "language_code" + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + @JvmStatic + val ALL_FIELDS = arrayOf( + COLUMN_NAME, + COLUMN_CODE + ) + + private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" + + private const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" + + "$COLUMN_NAME STRING," + + "$COLUMN_CODE STRING PRIMARY KEY" + + ");" + + /** + * This method creates a LanguagesTable in SQLiteDatabase + * @param db SQLiteDatabase + */ + @SuppressLint("SQLiteString") + @JvmStatic + fun onCreate(db: SQLiteDatabase) { + db.execSQL(CREATE_TABLE_STATEMENT) + } + + /** + * This method deletes LanguagesTable from SQLiteDatabase + * @param db SQLiteDatabase + */ + @JvmStatic + fun onDelete(db: SQLiteDatabase) { + db.execSQL(DROP_TABLE_STATEMENT) + onCreate(db) + } + + /** + * This method is called on migrating from a older version to a newer version + * @param db SQLiteDatabase + * @param from Version from which we are migrating + * @param to Version to which we are migrating + */ + @JvmStatic + fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) { + if (from == to) { + return + } + if (from < 19) { + // doesn't exist yet + onUpdate(db, from + 1, to) + return + } + if (from == 19) { + // table added in version 20 + onCreate(db) + onUpdate(db, from + 1, to) + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java deleted file mode 100644 index de01549478..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.java +++ /dev/null @@ -1,423 +0,0 @@ -package fr.free.nrw.commons.repository; - -import androidx.annotation.Nullable; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.category.CategoriesModel; -import fr.free.nrw.commons.category.CategoryItem; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.filepicker.UploadableFile; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.NearbyPlaces; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.upload.ImageCoordinates; -import fr.free.nrw.commons.upload.SimilarImageInterface; -import fr.free.nrw.commons.upload.UploadController; -import fr.free.nrw.commons.upload.UploadItem; -import fr.free.nrw.commons.upload.UploadModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictModel; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import io.reactivex.Flowable; -import io.reactivex.Observable; -import io.reactivex.Single; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import javax.inject.Inject; -import javax.inject.Singleton; -import timber.log.Timber; - -/** - * The repository class for UploadActivity - */ -@Singleton -public class UploadRepository { - - private final UploadModel uploadModel; - private final UploadController uploadController; - private final CategoriesModel categoriesModel; - private final NearbyPlaces nearbyPlaces; - private final DepictModel depictModel; - - private static final double NEARBY_RADIUS_IN_KILO_METERS = 0.1; //100 meters - private final ContributionDao contributionDao; - - @Inject - public UploadRepository(UploadModel uploadModel, - UploadController uploadController, - CategoriesModel categoriesModel, - NearbyPlaces nearbyPlaces, - DepictModel depictModel, - ContributionDao contributionDao) { - this.uploadModel = uploadModel; - this.uploadController = uploadController; - this.categoriesModel = categoriesModel; - this.nearbyPlaces = nearbyPlaces; - this.depictModel = depictModel; - this.contributionDao=contributionDao; - } - - /** - * asks the RemoteDataSource to build contributions - * - * @return - */ - public Observable buildContributions() { - return uploadModel.buildContributions(); - } - - /** - * asks the RemoteDataSource to start upload for the contribution - * - * @param contribution - */ - - public void prepareMedia(Contribution contribution) { - uploadController.prepareMedia(contribution); - } - - - public void saveContribution(Contribution contribution) { - contributionDao.save(contribution).blockingAwait(); - } - - /** - * Fetches and returns all the Upload Items - * - * @return - */ - public List getUploads() { - return uploadModel.getUploads(); - } - - /** - *Prepare for a fresh upload - */ - public void cleanup() { - uploadModel.cleanUp(); - //This needs further refactoring, this should not be here, right now the structure wont suppoort rhis - categoriesModel.cleanUp(); - depictModel.cleanUp(); - } - - /** - * Fetches and returns the selected categories for the current upload - * - * @return - */ - public List getSelectedCategories() { - return categoriesModel.getSelectedCategories(); - } - - /** - * all categories from MWApi - * - * @param query - * @param imageTitleList - * @param selectedDepictions - * @return - */ - public Observable> searchAll(String query, List imageTitleList, - List selectedDepictions) { - return categoriesModel.searchAll(query, imageTitleList, selectedDepictions); - } - - /** - * sets the list of selected categories for the current upload - * - * @param categoryStringList - */ - public void setSelectedCategories(List categoryStringList) { - uploadModel.setSelectedCategories(categoryStringList); - } - - /** - * handles the category selection/deselection - * - * @param categoryItem - */ - public void onCategoryClicked(CategoryItem categoryItem, final Media media) { - categoriesModel.onCategoryItemClicked(categoryItem, media); - } - - /** - * prunes the category list for irrelevant categories see #750 - * - * @param name - * @return - */ - public boolean isSpammyCategory(String name) { - return categoriesModel.isSpammyCategory(name); - } - - /** - * retursn the string list of available license from the LocalDataSource - * - * @return - */ - public List getLicenses() { - return uploadModel.getLicenses(); - } - - /** - * returns the selected license for the current upload - * - * @return - */ - public String getSelectedLicense() { - return uploadModel.getSelectedLicense(); - } - - /** - * returns the number of Upload Items - * - * @return - */ - public int getCount() { - return uploadModel.getCount(); - } - - /** - * ask the RemoteDataSource to pre process the image - * - * @param uploadableFile - * @param place - * @param similarImageInterface - * @return - */ - public Observable preProcessImage(UploadableFile uploadableFile, Place place, - SimilarImageInterface similarImageInterface, LatLng inAppPictureLocation) { - return uploadModel.preProcessImage(uploadableFile, place, - similarImageInterface, inAppPictureLocation); - } - - /** - * query the RemoteDataSource for image quality - * - * @param uploadItem UploadItem whose caption is to be checked - * @return Quality of UploadItem - */ - public Single getImageQuality(UploadItem uploadItem, LatLng location) { - return uploadModel.getImageQuality(uploadItem, location); - } - - /** - * query the RemoteDataSource for image duplicity check - * - * @param filePath file to be checked - * @return IMAGE_DUPLICATE or IMAGE_OK - */ - public Single checkDuplicateImage(String filePath) { - return uploadModel.checkDuplicateImage(filePath); - } - - /** - * query the RemoteDataSource for caption quality - * - * @param uploadItem UploadItem whose caption is to be checked - * @return Quality of caption of the UploadItem - */ - public Single getCaptionQuality(UploadItem uploadItem) { - return uploadModel.getCaptionQuality(uploadItem); - } - - /** - * asks the LocalDataSource to delete the file with the given file path - * - * @param filePath - */ - public void deletePicture(String filePath) { - uploadModel.deletePicture(filePath); - } - - /** - * fetches and returns the upload item - * - * @param index - * @return - */ - public UploadItem getUploadItem(int index) { - if (index >= 0) { - return uploadModel.getItems().get(index); - } - return null; //There is no item to copy details - } - - /** - * set selected license for the current upload - * - * @param licenseName - */ - public void setSelectedLicense(String licenseName) { - uploadModel.setSelectedLicense(licenseName); - } - - public void onDepictItemClicked(DepictedItem depictedItem, final Media media) { - uploadModel.onDepictItemClicked(depictedItem, media); - } - - /** - * Fetches and returns the selected depictions for the current upload - * - * @return - */ - - public List getSelectedDepictions() { - return uploadModel.getSelectedDepictions(); - } - - /** - * Provides selected existing depicts - * - * @return selected existing depicts - */ - public List getSelectedExistingDepictions() { - return uploadModel.getSelectedExistingDepictions(); - } - - /** - * Initialize existing depicts - * - * @param selectedExistingDepictions existing depicts - */ - public void setSelectedExistingDepictions(final List selectedExistingDepictions) { - uploadModel.setSelectedExistingDepictions(selectedExistingDepictions); - } - /** - * Search all depictions from - * - * @param query - * @return - */ - - public Flowable> searchAllEntities(String query) { - return depictModel.searchAllEntities(query, this); - } - - /** - * Gets the depiction for each unique {@link Place} associated with an {@link UploadItem} - * from {@link #getUploads()} - * - * @return a single that provides the depictions - */ - public Single> getPlaceDepictions() { - final Set qids = new HashSet<>(); - for (final UploadItem item : getUploads()) { - final Place place = item.getPlace(); - if (place != null) { - qids.add(place.getWikiDataEntityId()); - } - } - return depictModel.getPlaceDepictions(new ArrayList<>(qids)); - } - - /** - * Gets the category for each unique {@link Place} associated with an {@link UploadItem} - * from {@link #getUploads()} - * - * @return a single that provides the categories - */ - public Single> getPlaceCategories() { - final Set qids = new HashSet<>(); - for (final UploadItem item : getUploads()) { - final Place place = item.getPlace(); - if (place != null) { - qids.add(place.getCategory()); - } - } - return Single.fromObservable(categoriesModel.getCategoriesByName(new ArrayList<>(qids))); - } - - /** - * Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem - * from the server - * - * @param depictionsQIDs IDs of Depiction - * @return Flowable> - */ - public Flowable> getDepictions(final List depictionsQIDs){ - final String ids = joinQIDs(depictionsQIDs); - return depictModel.getDepictions(ids).toFlowable(); - } - - /** - * Builds a string by joining all IDs divided by "|" - * - * @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"] - * @return string ex. "Q11023|Q1356" - */ - private String joinQIDs(final List depictionsQIDs) { - if (depictionsQIDs != null && !depictionsQIDs.isEmpty()) { - final StringBuilder buffer = new StringBuilder(depictionsQIDs.get(0)); - - if (depictionsQIDs.size() > 1) { - for (int i = 1; i < depictionsQIDs.size(); i++) { - buffer.append("|"); - buffer.append(depictionsQIDs.get(i)); - } - } - return buffer.toString(); - } - return null; - } - - /** - * Returns nearest place matching the passed latitude and longitude - * - * @param decLatitude - * @param decLongitude - * @return - */ - @Nullable - public Place checkNearbyPlaces(final double decLatitude, final double decLongitude) { - try { - final List fromWikidataQuery = nearbyPlaces.getFromWikidataQuery(new LatLng( - decLatitude, decLongitude, 0.0f), - Locale.getDefault().getLanguage(), - NEARBY_RADIUS_IN_KILO_METERS, null); - return (fromWikidataQuery != null && fromWikidataQuery.size() > 0) ? fromWikidataQuery - .get(0) : null; - } catch (final Exception e) { - Timber.e("Error fetching nearby places: %s", e.getMessage()); - return null; - } - } - - public void useSimilarPictureCoordinates(ImageCoordinates imageCoordinates, int uploadItemIndex) { - uploadModel.useSimilarPictureCoordinates(imageCoordinates, uploadItemIndex); - } - - public boolean isWMLSupportedForThisPlace() { - return uploadModel.getItems().get(0).isWLMUpload(); - } - - /** - * Provides selected existing categories - * - * @return selected existing categories - */ - public List getSelectedExistingCategories() { - return categoriesModel.getSelectedExistingCategories(); - } - - /** - * Initialize existing categories - * - * @param selectedExistingCategories existing categories - */ - public void setSelectedExistingCategories(final List selectedExistingCategories) { - categoriesModel.setSelectedExistingCategories(selectedExistingCategories); - } - - /** - * Takes category names and Gets CategoryItem from the server - * - * @param categories names of Category - * @return Observable> - */ - public Observable> getCategories(final List categories){ - return categoriesModel.getCategoriesByName(categories); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt new file mode 100644 index 0000000000..0500f4946a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt @@ -0,0 +1,410 @@ +package fr.free.nrw.commons.repository + +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.category.CategoriesModel +import fr.free.nrw.commons.category.CategoryItem +import fr.free.nrw.commons.contributions.Contribution +import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.filepicker.UploadableFile +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.NearbyPlaces +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.upload.ImageCoordinates +import fr.free.nrw.commons.upload.SimilarImageInterface +import fr.free.nrw.commons.upload.UploadController +import fr.free.nrw.commons.upload.UploadItem +import fr.free.nrw.commons.upload.UploadModel +import fr.free.nrw.commons.upload.structure.depictions.DepictModel +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import io.reactivex.Flowable +import io.reactivex.Observable +import io.reactivex.Single +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +/** + * The repository class for UploadActivity + */ +@Singleton +class UploadRepository @Inject constructor( + private val uploadModel: UploadModel, + private val uploadController: UploadController, + private val categoriesModel: CategoriesModel, + private val nearbyPlaces: NearbyPlaces, + private val depictModel: DepictModel, + private val contributionDao: ContributionDao +) { + + companion object { + private const val NEARBY_RADIUS_IN_KILO_METERS = 0.1 // 100 meters + } + + /** + * Asks the RemoteDataSource to build contributions + * + * @return + */ + fun buildContributions(): Observable { + return uploadModel.buildContributions() + } + + /** + * Asks the RemoteDataSource to start upload for the contribution + * + * @param contribution + */ + fun prepareMedia(contribution: Contribution) { + uploadController.prepareMedia(contribution) + } + + fun saveContribution(contribution: Contribution) { + contributionDao.save(contribution).blockingAwait() + } + + /** + * Fetches and returns all the Upload Items + * + * @return + */ + fun getUploads(): List { + return uploadModel.getUploads() + } + + /** + * Prepare for a fresh upload + */ + fun cleanup() { + uploadModel.cleanUp() + // This needs further refactoring, this should not be here, right now the structure + // won't support this + categoriesModel.cleanUp() + depictModel.cleanUp() + } + + /** + * Fetches and returns the selected categories for the current upload + * + * @return + */ + fun getSelectedCategories(): List { + return categoriesModel.getSelectedCategories() + } + + /** + * All categories from MWApi + * + * @param query + * @param imageTitleList + * @param selectedDepictions + * @return + */ + fun searchAll( + query: String, + imageTitleList: List, + selectedDepictions: List + ): Observable> { + return categoriesModel.searchAll(query, imageTitleList, selectedDepictions) + } + + /** + * Sets the list of selected categories for the current upload + * + * @param categoryStringList + */ + fun setSelectedCategories(categoryStringList: List) { + uploadModel.setSelectedCategories(categoryStringList) + } + + /** + * Handles the category selection/deselection + * + * @param categoryItem + */ + fun onCategoryClicked(categoryItem: CategoryItem, media: Media?) { + categoriesModel.onCategoryItemClicked(categoryItem, media) + } + + /** + * Prunes the category list for irrelevant categories see #750 + * + * @param name + * @return + */ + fun isSpammyCategory(name: String): Boolean { + return categoriesModel.isSpammyCategory(name) + } + + /** + * Returns the string list of available licenses from the LocalDataSource + * + * @return + */ + fun getLicenses(): List { + return uploadModel.licenses + } + + /** + * Returns the selected license for the current upload + * + * @return + */ + fun getSelectedLicense(): String { + return uploadModel.selectedLicense + } + + /** + * Returns the number of Upload Items + * + * @return + */ + fun getCount(): Int { + return uploadModel.count + } + + /** + * Ask the RemoteDataSource to preprocess the image + * + * @param uploadableFile + * @param place + * @param similarImageInterface + * @param inAppPictureLocation + * @return + */ + fun preProcessImage( + uploadableFile: UploadableFile, + place: Place?, + similarImageInterface: SimilarImageInterface, + inAppPictureLocation: LatLng? + ): Observable { + return uploadModel.preProcessImage( + uploadableFile, + place, + similarImageInterface, + inAppPictureLocation + ) + } + + /** + * Query the RemoteDataSource for image quality + * + * @param uploadItem UploadItem whose caption is to be checked + * @param location Location of the image + * @return Quality of UploadItem + */ + fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single { + return uploadModel.getImageQuality(uploadItem, location) + } + + /** + * Query the RemoteDataSource for image duplicity check + * + * @param filePath file to be checked + * @return IMAGE_DUPLICATE or IMAGE_OK + */ + fun checkDuplicateImage(filePath: String): Single { + return uploadModel.checkDuplicateImage(filePath) + } + + /** + * query the RemoteDataSource for caption quality + * + * @param uploadItem UploadItem whose caption is to be checked + * @return Quality of caption of the UploadItem + */ + fun getCaptionQuality(uploadItem: UploadItem): Single { + return uploadModel.getCaptionQuality(uploadItem) + } + + /** + * asks the LocalDataSource to delete the file with the given file path + * + * @param filePath + */ + fun deletePicture(filePath: String) { + uploadModel.deletePicture(filePath) + } + + /** + * fetches and returns the upload item + * + * @param index + * @return + */ + fun getUploadItem(index: Int): UploadItem? { + return if (index >= 0) { + uploadModel.items.getOrNull(index) + } else null //There is no item to copy details + } + + /** + * set selected license for the current upload + * + * @param licenseName + */ + fun setSelectedLicense(licenseName: String) { + uploadModel.selectedLicense = licenseName + } + + fun onDepictItemClicked(depictedItem: DepictedItem, media: Media?) { + uploadModel.onDepictItemClicked(depictedItem, media) + } + + /** + * Fetches and returns the selected depictions for the current upload + * + * @return + */ + fun getSelectedDepictions(): List { + return uploadModel.selectedDepictions + } + + /** + * Provides selected existing depicts + * + * @return selected existing depicts + */ + fun getSelectedExistingDepictions(): List { + return uploadModel.selectedExistingDepictions + } + + /** + * Initialize existing depicts + * + * @param selectedExistingDepictions existing depicts + */ + fun setSelectedExistingDepictions(selectedExistingDepictions: List) { + uploadModel.selectedExistingDepictions = selectedExistingDepictions + } + + /** + * Search all depictions from + * + * @param query + * @return + */ + fun searchAllEntities(query: String): Flowable> { + return depictModel.searchAllEntities(query, this) + } + + /** + * Gets the depiction for each unique {@link Place} associated with an {@link UploadItem} + * from {@link #getUploads()} + * + * @return a single that provides the depictions + */ + fun getPlaceDepictions(): Single> { + val qids = mutableSetOf() + getUploads().forEach { item -> + item.place?.let { + it.wikiDataEntityId?.let { it1 -> + qids.add(it1) + } + } + } + return depictModel.getPlaceDepictions(qids.toList()) + } + + /** + * Gets the category for each unique {@link Place} associated with an {@link UploadItem} + * from {@link #getUploads()} + * + * @return a single that provides the categories + */ + fun getPlaceCategories(): Single> { + val qids = mutableSetOf() + getUploads().forEach { item -> + item.place?.category?.let { qids.add(it) } + } + return Single.fromObservable(categoriesModel.getCategoriesByName(qids.toList())) + } + + /** + * Takes depict IDs as a parameter, converts into a slash separated String and Gets DepictItem + * from the server + * + * @param depictionsQIDs IDs of Depiction + * @return Flowable> + */ + fun getDepictions(depictionsQIDs: List): Flowable> { + val ids = joinQIDs(depictionsQIDs) ?: "" + return depictModel.getDepictions(ids).toFlowable() + } + + /** + * Builds a string by joining all IDs divided by "|" + * + * @param depictionsQIDs IDs of depiction ex. ["Q11023","Q1356"] + * @return string ex. "Q11023|Q1356" + */ + private fun joinQIDs(depictionsQIDs: List?): String? { + return depictionsQIDs?.takeIf { + it.isNotEmpty() + }?.joinToString("|") + } + + /** + * Returns nearest place matching the passed latitude and longitude + * + * @param decLatitude + * @param decLongitude + * @return + */ + fun checkNearbyPlaces(decLatitude: Double, decLongitude: Double): Place? { + return try { + val fromWikidataQuery = nearbyPlaces.getFromWikidataQuery( + LatLng(decLatitude, decLongitude, 0.0f), + Locale.getDefault().language, + NEARBY_RADIUS_IN_KILO_METERS, + null + ) + fromWikidataQuery?.firstOrNull() + } catch (e: Exception) { + Timber.e("Error fetching nearby places: %s", e.message) + null + } + } + + fun useSimilarPictureCoordinates(imageCoordinates: ImageCoordinates, uploadItemIndex: Int) { + uploadModel.useSimilarPictureCoordinates( + imageCoordinates, + uploadItemIndex + ) + } + + fun isWMLSupportedForThisPlace(): Boolean { + return uploadModel.items.firstOrNull()?.isWLMUpload == true + } + + /** + * Provides selected existing categories + * + * @return selected existing categories + */ + fun getSelectedExistingCategories(): List { + return categoriesModel.getSelectedExistingCategories() + } + + /** + * Initialize existing categories + * + * @param selectedExistingCategories existing categories + */ + fun setSelectedExistingCategories(selectedExistingCategories: List) { + categoriesModel.setSelectedExistingCategories( + selectedExistingCategories.toMutableList() + ) + } + + /** + * Takes category names and Gets CategoryItem from the server + * + * @param categories names of Category + * @return Observable> + */ + fun getCategories(categories: List): Observable> { + return categoriesModel.getCategoriesByName(categories) + ?.map { it.toList() } ?: Observable.empty() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 1822df8302..712f6fc3ed 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -61,7 +61,7 @@ class CategoriesPresenter .doOnNext { view.showProgress(true) }.switchMap(::searchResults) - .map { repository.selectedCategories + it } + .map { repository.getSelectedCategories() + it } .map { it.distinctBy { categoryItem -> categoryItem.name } } .observeOn(mainThreadScheduler) .subscribe( @@ -89,7 +89,7 @@ class CategoriesPresenter private fun searchResults(term: String): Observable>? { if (media == null) { return repository - .searchAll(term, getImageTitleList(), repository.selectedDepictions) + .searchAll(term, getImageTitleList(), repository.getSelectedDepictions()) .subscribeOn(ioScheduler) .map { it.filter { categoryItem -> @@ -101,13 +101,13 @@ class CategoriesPresenter return Observable .zip( repository - .getCategories(repository.selectedExistingCategories) + .getCategories(repository.getSelectedExistingCategories()) .map { list -> list.map { CategoryItem(it.name, it.description, it.thumbnail, true) } }, - repository.searchAll(term, getImageTitleList(), repository.selectedDepictions), + repository.searchAll(term, getImageTitleList(), repository.getSelectedDepictions()), ) { it1, it2 -> it1 + it2 }.subscribeOn(ioScheduler) @@ -138,7 +138,7 @@ class CategoriesPresenter * @return */ private fun getImageTitleList(): List = - repository.uploads + repository.getUploads() .map { it.uploadMediaDetails[0].captionText } .filterNot { TextUtils.isEmpty(it) } @@ -146,7 +146,7 @@ class CategoriesPresenter * Verifies the number of categories selected, prompts the user if none selected */ override fun verifyCategories() { - val selectedCategories = repository.selectedCategories + val selectedCategories = repository.getSelectedCategories() if (selectedCategories.isNotEmpty()) { repository.setSelectedCategories(selectedCategories.map { it.name }) view.goToNextScreen() @@ -173,14 +173,14 @@ class CategoriesPresenter ) { this.view = view this.media = media - repository.selectedExistingCategories = view.existingCategories + repository.setSelectedExistingCategories(view.existingCategories) compositeDisposable.add( searchTerms .observeOn(mainThreadScheduler) .doOnNext { view.showProgress(true) }.switchMap(::searchResults) - .map { repository.selectedCategories + it } + .map { repository.getSelectedCategories() + it } .map { it.distinctBy { categoryItem -> categoryItem.name } } .observeOn(mainThreadScheduler) .subscribe( @@ -218,13 +218,21 @@ class CategoriesPresenter wikiText: String, ) { // check if view.existingCategories is null - if (repository.selectedCategories.isNotEmpty() || - (view.existingCategories != null && repository.selectedExistingCategories.size != view.existingCategories.size) + if ( + repository.getSelectedCategories().isNotEmpty() + || + ( + view.existingCategories != null + && + repository.getSelectedExistingCategories().size + != + view.existingCategories.size + ) ) { val selectedCategories: MutableList = ( - repository.selectedCategories.map { it.name }.toMutableList() + - repository.selectedExistingCategories + repository.getSelectedCategories().map { it.name }.toMutableList() + + repository.getSelectedExistingCategories() ).toMutableList() if (selectedCategories.isNotEmpty()) { @@ -305,7 +313,7 @@ class CategoriesPresenter override fun selectCategories() { compositeDisposable.add( - repository.placeCategories + repository.getPlaceCategories() .subscribeOn(ioScheduler) .observeOn(mainThreadScheduler) .subscribe(::selectNewCategories), diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt index 3beedd9d5c..fa3eb354ee 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt @@ -93,14 +93,14 @@ class DepictsPresenter return repository .searchAllEntities(querystring) .subscribeOn(ioScheduler) - .map { repository.selectedDepictions + it + recentDepictedItemList + controller.loadFavoritesItems() } + .map { repository.getSelectedDepictions() + it + recentDepictedItemList + controller.loadFavoritesItems() } .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } .map { it.distinctBy(DepictedItem::id) } } else { return Flowable .zip( repository - .getDepictions(repository.selectedExistingDepictions) + .getDepictions(repository.getSelectedExistingDepictions()) .map { list -> list.map { DepictedItem( @@ -118,7 +118,7 @@ class DepictsPresenter ) { it1, it2 -> it1 + it2 }.subscribeOn(ioScheduler) - .map { repository.selectedDepictions + it + recentDepictedItemList + controller.loadFavoritesItems() } + .map { repository.getSelectedDepictions() + it + recentDepictedItemList + controller.loadFavoritesItems() } .map { it.filterNot { item -> WikidataDisambiguationItems.isDisambiguationItem(item.instanceOfs) } } .map { it.distinctBy(DepictedItem::id) } } @@ -135,7 +135,7 @@ class DepictsPresenter */ override fun selectPlaceDepictions() { compositeDisposable.add( - repository.placeDepictions + repository.getPlaceDepictions() .subscribeOn(ioScheduler) .observeOn(mainThreadScheduler) .subscribe(::selectNewDepictions), @@ -188,10 +188,10 @@ class DepictsPresenter * from the depiction list */ override fun verifyDepictions() { - if (repository.selectedDepictions.isNotEmpty()) { + if (repository.getSelectedDepictions().isNotEmpty()) { if (::depictsDao.isInitialized) { // save all the selected Depicted item in room Database - depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions) + depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions()) } view.goToNextScreen() } else { @@ -205,20 +205,20 @@ class DepictsPresenter */ @SuppressLint("CheckResult") override fun updateDepictions(media: Media) { - if (repository.selectedDepictions.isNotEmpty() || - repository.selectedExistingDepictions.size != view.existingDepictions.size + if (repository.getSelectedDepictions().isNotEmpty() || + repository.getSelectedExistingDepictions().size != view.existingDepictions.size ) { view.showProgressDialog() val selectedDepictions: MutableList = ( - repository.selectedDepictions.map { it.id }.toMutableList() + - repository.selectedExistingDepictions + repository.getSelectedDepictions().map { it.id }.toMutableList() + + repository.getSelectedExistingDepictions() ).toMutableList() if (selectedDepictions.isNotEmpty()) { if (::depictsDao.isInitialized) { // save all the selected Depicted item in room Database - depictsDao.savingDepictsInRoomDataBase(repository.selectedDepictions) + depictsDao.savingDepictsInRoomDataBase(repository.getSelectedDepictions()) } compositeDisposable.add( @@ -254,7 +254,7 @@ class DepictsPresenter ) { this.view = view this.media = media - repository.selectedExistingDepictions = view.existingDepictions + repository.setSelectedExistingDepictions(view.existingDepictions) compositeDisposable.add( searchTerm .observeOn(mainThreadScheduler) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt index 7242b8eed3..9337cb8b58 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt @@ -39,7 +39,7 @@ class DepictModel for (place in places) { place.wikiDataEntityId?.let { qids.add(it) } } - repository.uploads.forEach { item -> + repository.getUploads().forEach { item -> if (item.gpsCoords != null && item.gpsCoords.imageCoordsExists) { Coordinates2Country .countryQID( diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt index 4b321071fe..bb8fd1fc5e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/CategoriesPresenterTest.kt @@ -56,9 +56,9 @@ class CategoriesPresenterTest { @Throws(Exception::class) fun `Test onAttachViewWithMedia when media is not null`() { categoriesPresenter.onAttachViewWithMedia(view, media()) - whenever(repository.getCategories(repository.selectedExistingCategories)) + whenever(repository.getCategories(repository.getSelectedExistingCategories())) .thenReturn(Observable.just(mutableListOf(categoryItem()))) - whenever(repository.searchAll("mock", emptyList(), repository.selectedDepictions)) + whenever(repository.searchAll("mock", emptyList(), repository.getSelectedDepictions())) .thenReturn(Observable.just(mutableListOf(categoryItem()))) val method: Method = CategoriesPresenter::class.java.getDeclaredMethod( @@ -88,7 +88,7 @@ class CategoriesPresenterTest { val emptyCaptionUploadItem = mock() whenever(emptyCaptionUploadItem.uploadMediaDetails) .thenReturn(listOf(UploadMediaDetail(captionText = ""))) - whenever(repository.uploads).thenReturn( + whenever(repository.getUploads()).thenReturn( listOf( nonEmptyCaptionUploadItem, emptyCaptionUploadItem, @@ -105,7 +105,7 @@ class CategoriesPresenterTest { ) whenever(repository.isSpammyCategory("selected")).thenReturn(false) whenever(repository.isSpammyCategory("doesContainYear")).thenReturn(true) - whenever(repository.selectedCategories).thenReturn( + whenever(repository.getSelectedCategories()).thenReturn( listOf( categoryItem("selected", "", "", true), ), @@ -130,7 +130,7 @@ class CategoriesPresenterTest { whenever(repository.searchAll(any(), any(), any())) .thenReturn(Observable.just(emptyCategories)) - whenever(repository.selectedCategories).thenReturn(listOf()) + whenever(repository.getSelectedCategories()).thenReturn(listOf()) categoriesPresenter.searchForCategories(query) testScheduler.triggerActions() val method: Method = @@ -154,7 +154,7 @@ class CategoriesPresenterTest { fun `verifyCategories with non empty selection goes to next screen`() { categoriesPresenter.onAttachView(view) val item = categoryItem() - whenever(repository.selectedCategories).thenReturn(listOf(item)) + whenever(repository.getSelectedCategories()).thenReturn(listOf(item)) categoriesPresenter.verifyCategories() verify(repository).setSelectedCategories(listOf(item.name)) verify(view).goToNextScreen() @@ -163,7 +163,7 @@ class CategoriesPresenterTest { @Test fun `verifyCategories with empty selection show no category selected`() { categoriesPresenter.onAttachView(view) - whenever(repository.selectedCategories).thenReturn(listOf()) + whenever(repository.getSelectedCategories()).thenReturn(listOf()) categoriesPresenter.verifyCategories() verify(view).showNoCategorySelected() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt index 748b95ea6d..1abff908e2 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/DepictsPresenterTest.kt @@ -74,7 +74,7 @@ class DepictsPresenterTest { ) whenever(repository.searchAllEntities("")).thenReturn(Flowable.just(searchResults)) val selectedItem = depictedItem(id = "selected") - whenever(repository.selectedDepictions).thenReturn(listOf(selectedItem)) + whenever(repository.getSelectedDepictions()).thenReturn(listOf(selectedItem)) depictsPresenter.searchForDepictions("") testScheduler.triggerActions() verify(view).showProgress(false) @@ -123,14 +123,14 @@ class DepictsPresenterTest { @Test fun `verifyDepictions with non empty selectedDepictions goes to next screen`() { - whenever(repository.selectedDepictions).thenReturn(listOf(depictedItem())) + whenever(repository.getSelectedDepictions()).thenReturn(listOf(depictedItem())) depictsPresenter.verifyDepictions() verify(view).goToNextScreen() } @Test fun `verifyDepictions with empty selectedDepictions goes to noDepictionSelected`() { - whenever(repository.selectedDepictions).thenReturn(emptyList()) + whenever(repository.getSelectedDepictions()).thenReturn(emptyList()) depictsPresenter.verifyDepictions() verify(view).noDepictionSelected() } @@ -162,7 +162,7 @@ class DepictsPresenterTest { @Test fun `Test searchResults when media is not null`() { Whitebox.setInternalState(depictsPresenter, "media", media) - whenever(repository.getDepictions(repository.selectedExistingDepictions)) + whenever(repository.getDepictions(repository.getSelectedExistingDepictions())) .thenReturn(Flowable.just(listOf(depictedItem()))) whenever(repository.searchAllEntities("querystring")) .thenReturn(Flowable.just(listOf(depictedItem()))) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index bbaaaec1cf..29a35c1e55 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -84,7 +84,7 @@ class UploadPresenterTest { fun handleSubmitImagesNoLocationWithConsecutiveNoLocationUploads() { `when`(imageCoords.imageCoordsExists).thenReturn(false) `when`(uploadItem.getGpsCoords()).thenReturn(imageCoords) - `when`(repository.uploads).thenReturn(uploadableItems) + `when`(repository.getUploads()).thenReturn(uploadableItems) uploadableItems.add(uploadItem) // test 1 - insufficient count @@ -112,7 +112,7 @@ class UploadPresenterTest { ).thenReturn(UploadPresenter.CONSECUTIVE_UPLOADS_WITHOUT_COORDINATES_REMINDER_THRESHOLD) `when`(imageCoords.imageCoordsExists).thenReturn(true) `when`(uploadItem.getGpsCoords()).thenReturn(imageCoords) - `when`(repository.uploads).thenReturn(uploadableItems) + `when`(repository.getUploads()).thenReturn(uploadableItems) uploadableItems.add(uploadItem) uploadPresenter.handleSubmit() // no alert dialog expected diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt index 54d35494a3..233b0de32c 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt @@ -117,7 +117,7 @@ class UploadRepositoryUnitTest { @Test fun testGetUploads() { - assertEquals(repository.uploads, uploadModel.uploads) + assertEquals(repository.getUploads(), uploadModel.uploads) } @Test @@ -130,7 +130,7 @@ class UploadRepositoryUnitTest { @Test fun testGetSelectedCategories() { - assertEquals(repository.selectedCategories, categoriesModel.getSelectedCategories()) + assertEquals(repository.getSelectedCategories(), categoriesModel.getSelectedCategories()) } @Test @@ -163,17 +163,17 @@ class UploadRepositoryUnitTest { @Test fun testGetLicenses() { - assertEquals(repository.licenses, uploadModel.licenses) + assertEquals(repository.getLicenses(), uploadModel.licenses) } @Test fun testGetSelectedLicense() { - assertEquals(repository.selectedLicense, uploadModel.selectedLicense) + assertEquals(repository.getSelectedLicense(), uploadModel.selectedLicense) } @Test fun testGetCount() { - assertEquals(repository.count, uploadModel.count) + assertEquals(repository.getCount(), uploadModel.count) } @Test @@ -242,12 +242,12 @@ class UploadRepositoryUnitTest { @Test fun testGetSelectedDepictions() { - assertEquals(repository.selectedDepictions, uploadModel.selectedDepictions) + assertEquals(repository.getSelectedDepictions(), uploadModel.selectedDepictions) } @Test fun testGetSelectedExistingDepictions() { - assertEquals(repository.selectedExistingDepictions, uploadModel.selectedExistingDepictions) + assertEquals(repository.getSelectedExistingDepictions(), uploadModel.selectedExistingDepictions) } @Test @@ -264,7 +264,7 @@ class UploadRepositoryUnitTest { `when`(uploadItem.place).thenReturn(place) `when`(place.wikiDataEntityId).thenReturn("1") assertEquals( - repository.placeDepictions, + repository.getPlaceDepictions(), depictModel.getPlaceDepictions(listOf("1")), ) } @@ -326,7 +326,7 @@ class UploadRepositoryUnitTest { `when`(uploadModel.items).thenReturn(listOf(uploadItem)) `when`(uploadItem.isWLMUpload).thenReturn(true) assertEquals( - repository.isWMLSupportedForThisPlace, + repository.isWMLSupportedForThisPlace(), true, ) } @@ -369,7 +369,7 @@ class UploadRepositoryUnitTest { @Test fun testGetSelectedExistingCategories() { assertEquals( - repository.selectedExistingCategories, + repository.getSelectedExistingCategories(), categoriesModel.getSelectedExistingCategories(), ) } From e070c5dbe89b31021fd4bccf9f836710c9616b37 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Sat, 23 Nov 2024 05:05:34 +0530 Subject: [PATCH 031/231] Fix unit tests (#5947) * move createLocale() method to companion object and add test dependency * use mockk() from Mockk library for mocking sealed classes * change method parameter to null-able String type * add null check for accessing property from unit tests * change method signature to match old method's signature It fixes the NullPointerException when running ImageProcessingUnitTest * Fix unresolved references and make properties public for unit tests * fix tests in UploadRepositoryUnitTest by making return type null-able --- app/build.gradle | 1 + .../nrw/commons/category/CategoryClient.kt | 4 ++- .../recentlanguages/RecentLanguagesDao.kt | 5 ++-- .../commons/repository/UploadRepository.kt | 16 ++++++------ .../nrw/commons/settings/SettingsFragment.kt | 26 ++++++++++--------- .../nrw/commons/utils/ImageUtilsWrapper.kt | 2 +- .../commons/utils/MediaDataExtractorUtil.kt | 8 ++---- .../commons/auth/LoginActivityUnitTests.kt | 3 ++- .../commons/auth/SessionManagerUnitTests.kt | 9 ++++--- .../RecentLanguagesDaoUnitTest.kt | 10 +++---- .../settings/SettingsFragmentUnitTests.kt | 8 +++--- .../upload/UploadMediaPresenterTest.kt | 6 ++--- .../upload/UploadRepositoryUnitTest.kt | 6 +++-- .../structure/depictions/DepictedItemTest.kt | 4 +-- 14 files changed, 56 insertions(+), 52 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 9e6c56c83f..468255d38c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,6 +98,7 @@ dependencies { testImplementation 'org.mockito:mockito-core:5.6.0' testImplementation "org.powermock:powermock-module-junit4:2.0.9" testImplementation "org.powermock:powermock-api-mockito2:2.0.9" + testImplementation("io.mockk:mockk:1.13.5") // Unit testing testImplementation 'junit:junit:4.13.2' diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 992c4ed1cf..5571e0ea77 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt @@ -124,7 +124,9 @@ class CategoryClient }.map { it .filter { page -> - !page.categoryInfo().isHidden + // Null check is not redundant because some values could be null + // for mocks when running unit tests + page.categoryInfo()?.isHidden != true }.map { CategoryItem( it.title().replace(CATEGORY_PREFIX, ""), diff --git a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt index e97c4f8165..a4a06185bf 100644 --- a/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt @@ -6,7 +6,6 @@ import android.content.ContentValues import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.os.RemoteException -import java.util.ArrayList import javax.inject.Inject import javax.inject.Named import javax.inject.Provider @@ -163,9 +162,9 @@ class RecentLanguagesDao @Inject constructor( COLUMN_CODE ) - private const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" + const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" - private const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" + + const val CREATE_TABLE_STATEMENT = "CREATE TABLE $TABLE_NAME (" + "$COLUMN_NAME STRING," + "$COLUMN_CODE STRING PRIMARY KEY" + ");" diff --git a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt index 0500f4946a..3779532547 100644 --- a/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt +++ b/app/src/main/java/fr/free/nrw/commons/repository/UploadRepository.kt @@ -19,10 +19,10 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import io.reactivex.Flowable import io.reactivex.Observable import io.reactivex.Single +import timber.log.Timber import java.util.Locale import javax.inject.Inject import javax.inject.Singleton -import timber.log.Timber /** * The repository class for UploadActivity @@ -46,7 +46,7 @@ class UploadRepository @Inject constructor( * * @return */ - fun buildContributions(): Observable { + fun buildContributions(): Observable? { return uploadModel.buildContributions() } @@ -150,7 +150,7 @@ class UploadRepository @Inject constructor( * * @return */ - fun getSelectedLicense(): String { + fun getSelectedLicense(): String? { return uploadModel.selectedLicense } @@ -173,11 +173,11 @@ class UploadRepository @Inject constructor( * @return */ fun preProcessImage( - uploadableFile: UploadableFile, + uploadableFile: UploadableFile?, place: Place?, - similarImageInterface: SimilarImageInterface, + similarImageInterface: SimilarImageInterface?, inAppPictureLocation: LatLng? - ): Observable { + ): Observable? { return uploadModel.preProcessImage( uploadableFile, place, @@ -193,7 +193,7 @@ class UploadRepository @Inject constructor( * @param location Location of the image * @return Quality of UploadItem */ - fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single { + fun getImageQuality(uploadItem: UploadItem, location: LatLng?): Single? { return uploadModel.getImageQuality(uploadItem, location) } @@ -213,7 +213,7 @@ class UploadRepository @Inject constructor( * @param uploadItem UploadItem whose caption is to be checked * @return Quality of caption of the UploadItem */ - fun getCaptionQuality(uploadItem: UploadItem): Single { + fun getCaptionQuality(uploadItem: UploadItem): Single? { return uploadModel.getCaptionQuality(uploadItem) } diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index 53f6b28fe2..b55ac60099 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -471,18 +471,20 @@ class SettingsFragment : PreferenceFragmentCompat() { editor.apply() } - /** - * Create Locale based on different types of language codes - * @param languageCode - * @return Locale and throws error for invalid language codes - */ - fun createLocale(languageCode: String): Locale { - val parts = languageCode.split("-") - return when (parts.size) { - 1 -> Locale(parts[0]) - 2 -> Locale(parts[0], parts[1]) - 3 -> Locale(parts[0], parts[1], parts[2]) - else -> throw IllegalArgumentException("Invalid language code: $languageCode") + companion object { + /** + * Create Locale based on different types of language codes + * @param languageCode + * @return Locale and throws error for invalid language codes + */ + fun createLocale(languageCode: String): Locale { + val parts = languageCode.split("-") + return when (parts.size) { + 1 -> Locale(parts[0]) + 2 -> Locale(parts[0], parts[1]) + 3 -> Locale(parts[0], parts[1], parts[2]) + else -> throw IllegalArgumentException("Invalid language code: $languageCode") + } } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt index 2e0efc6901..8393dc6524 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtilsWrapper.kt @@ -16,7 +16,7 @@ class ImageUtilsWrapper @Inject constructor() { fun checkImageGeolocationIsDifferent( geolocationOfFileString: String, - latLng: LatLng + latLng: LatLng? ): Single { return Single.fromCallable { ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng) diff --git a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt index 9e46525da5..93cdabbfc0 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt @@ -1,9 +1,5 @@ package fr.free.nrw.commons.utils -import org.apache.commons.lang3.StringUtils - -import java.util.ArrayList - object MediaDataExtractorUtil { /** @@ -13,8 +9,8 @@ object MediaDataExtractorUtil { * @return */ @JvmStatic - fun extractCategoriesFromList(source: String): List { - if (source.isBlank()) { + fun extractCategoriesFromList(source: String?): List { + if (source.isNullOrBlank()) { return emptyList() } val cats = source.split("|") diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt index b50c820a0d..162f505848 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt @@ -16,6 +16,7 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.auth.login.LoginResult import fr.free.nrw.commons.createTestClient import fr.free.nrw.commons.kvstore.JsonKvStore +import io.mockk.mockk import org.junit.Assert import org.junit.Before import org.junit.Test @@ -66,11 +67,11 @@ class LoginActivityUnitTests { @Mock private lateinit var account: Account - @Mock private lateinit var loginResult: LoginResult @Before fun setUp() { + loginResult = mockk() MockitoAnnotations.openMocks(this) OkHttpConnectionFactory.CLIENT = createTestClient() activity = Robolectric.buildActivity(LoginActivity::class.java).create().get() diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt index 7b7c260e86..4e5c78f3e5 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/SessionManagerUnitTests.kt @@ -7,13 +7,14 @@ import androidx.test.core.app.ApplicationProvider import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.auth.login.LoginResult import fr.free.nrw.commons.kvstore.JsonKvStore +import io.mockk.every +import io.mockk.mockk import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations -import org.powermock.api.mockito.PowerMockito.`when` import org.robolectric.RobolectricTestRunner import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -33,7 +34,6 @@ class SessionManagerUnitTests { @Mock private lateinit var defaultKvStore: JsonKvStore - @Mock private lateinit var loginResult: LoginResult @Mock @@ -41,6 +41,7 @@ class SessionManagerUnitTests { @Before fun setUp() { + loginResult = mockk() MockitoAnnotations.openMocks(this) accountManager = AccountManager.get(ApplicationProvider.getApplicationContext()) shadowOf(accountManager).addAccount(account) @@ -68,8 +69,8 @@ class SessionManagerUnitTests { @Test @Throws(Exception::class) fun testUpdateAccount() { - `when`(loginResult.userName).thenReturn("username") - `when`(loginResult.password).thenReturn("password") + every { loginResult.userName } returns "username" + every { loginResult.password } returns "password" val method: Method = SessionManager::class.java.getDeclaredMethod( "updateAccount", diff --git a/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt index 087640a44c..e0f4587f46 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/recentlanguages/RecentLanguagesDaoUnitTest.kt @@ -85,7 +85,7 @@ class RecentLanguagesDaoUnitTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) .thenReturn(createCursor(14)) - val result = testObject.recentLanguages + val result = testObject.getRecentLanguages() Assert.assertEquals(14, (result.size)) } @@ -95,20 +95,20 @@ class RecentLanguagesDaoUnitTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenThrow( RemoteException(""), ) - testObject.recentLanguages + testObject.getRecentLanguages() } @Test fun getGetRecentLanguagesReturnsEmptyList_emptyCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())) .thenReturn(createCursor(0)) - Assert.assertTrue(testObject.recentLanguages.isEmpty()) + Assert.assertTrue(testObject.getRecentLanguages().isEmpty()) } @Test fun getGetRecentLanguagesReturnsEmptyList_nullCursor() { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(null) - Assert.assertTrue(testObject.recentLanguages.isEmpty()) + Assert.assertTrue(testObject.getRecentLanguages().isEmpty()) } @Test @@ -117,7 +117,7 @@ class RecentLanguagesDaoUnitTest { whenever(client.query(any(), any(), anyOrNull(), any(), anyOrNull())).thenReturn(mockCursor) whenever(mockCursor.moveToFirst()).thenReturn(false) - testObject.recentLanguages + testObject.getRecentLanguages() verify(mockCursor).close() } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt index 6c9ee9d039..5a6d27e1bb 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/settings/SettingsFragmentUnitTests.kt @@ -19,7 +19,7 @@ import fr.free.nrw.commons.TestCommonsApplication import fr.free.nrw.commons.recentlanguages.Language import fr.free.nrw.commons.recentlanguages.RecentLanguagesAdapter import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao -import fr.free.nrw.commons.settings.SettingsFragment.createLocale +import fr.free.nrw.commons.settings.SettingsFragment.Companion.createLocale import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before @@ -156,14 +156,14 @@ class SettingsFragmentUnitTests { ) method.isAccessible = true method.invoke(fragment, "appUiDefaultLanguagePref") - verify(recentLanguagesDao, times(1)).recentLanguages + verify(recentLanguagesDao, times(1)).getRecentLanguages() } @Test @Throws(Exception::class) fun `Test prepareAppLanguages when recently used languages is not empty`() { Shadows.shadowOf(Looper.getMainLooper()).idle() - whenever(recentLanguagesDao.recentLanguages) + whenever(recentLanguagesDao.getRecentLanguages()) .thenReturn( mutableListOf( Language("English", "en"), @@ -181,7 +181,7 @@ class SettingsFragmentUnitTests { ) method.isAccessible = true method.invoke(fragment, "appUiDefaultLanguagePref") - verify(recentLanguagesDao, times(2)).recentLanguages + verify(recentLanguagesDao, times(2)).getRecentLanguages() } @Test diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt index bf27280aae..47c4d0ae56 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadMediaPresenterTest.kt @@ -131,7 +131,7 @@ class UploadMediaPresenterTest { */ @Test fun getImageQualityTest() { - whenever(repository.uploads).thenReturn(listOf(uploadItem)) + whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getImageQuality(uploadItem, location)) .thenReturn(testSingleImageResult) whenever(uploadItem.imageQuality).thenReturn(0) @@ -149,7 +149,7 @@ class UploadMediaPresenterTest { */ @Test fun `get ImageQuality Test while coordinates equals to null`() { - whenever(repository.uploads).thenReturn(listOf(uploadItem)) + whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getImageQuality(uploadItem, location)) .thenReturn(testSingleImageResult) whenever(uploadItem.imageQuality).thenReturn(0) @@ -225,7 +225,7 @@ class UploadMediaPresenterTest { */ @Test fun fetchImageAndTitleTest() { - whenever(repository.uploads).thenReturn(listOf(uploadItem)) + whenever(repository.getUploads()).thenReturn(listOf(uploadItem)) whenever(repository.getUploadItem(ArgumentMatchers.anyInt())) .thenReturn(uploadItem) whenever(uploadItem.uploadMediaDetails).thenReturn(listOf()) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt index 233b0de32c..ac01d237fe 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadRepositoryUnitTest.kt @@ -15,6 +15,7 @@ import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.structure.depictions.DepictModel import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import io.reactivex.Completable +import io.reactivex.Observable import io.reactivex.Single import org.junit.Before import org.junit.Test @@ -196,7 +197,7 @@ class UploadRepositoryUnitTest { fun testGetCaptionQuality() { assertEquals( repository.getCaptionQuality(uploadItem), - uploadModel.getCaptionQuality(uploadItem), + uploadModel.getCaptionQuality(uploadItem) ) } @@ -386,7 +387,8 @@ class UploadRepositoryUnitTest { fun testGetCategories() { assertEquals( repository.getCategories(listOf("Test")), - categoriesModel.getCategoriesByName(mutableListOf("Test")), + categoriesModel.getCategoriesByName(mutableListOf("Test")) + ?: Observable.empty>() ) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt index 892d501fd2..e0d339eee8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/structure/depictions/DepictedItemTest.kt @@ -1,10 +1,10 @@ package fr.free.nrw.commons.upload.structure.depictions -import com.nhaarman.mockitokotlin2.mock import depictedItem import entity import entityId import fr.free.nrw.commons.wikidata.WikidataProperties +import io.mockk.mockk import org.junit.Assert import org.junit.Test import place @@ -53,7 +53,7 @@ class DepictedItemTest { entity( statements = mapOf( - WikidataProperties.IMAGE.propertyName to listOf(statement(snak(dataValue = mock()))), + WikidataProperties.IMAGE.propertyName to listOf(statement(snak(dataValue = mockk()))), ), ), ).imageUrl, From bafae821e26bd71e351a6a97f4815aca6c502cd1 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Sat, 23 Nov 2024 18:15:46 +0530 Subject: [PATCH 032/231] Migration of review module from Java to Kotlin (#5950) * Rename .java to .kt * Migrated repository module to Kotlin * Rename .java to .kt * Migrated review module to Kotlin --- .../navtab/MoreBottomSheetFragment.java | 2 +- .../nrw/commons/review/ReviewActivity.java | 334 ----------------- .../free/nrw/commons/review/ReviewActivity.kt | 336 ++++++++++++++++++ .../nrw/commons/review/ReviewController.java | 220 ------------ .../nrw/commons/review/ReviewController.kt | 231 ++++++++++++ .../review/{ReviewDao.java => ReviewDao.kt} | 21 +- .../free/nrw/commons/review/ReviewEntity.java | 19 - .../free/nrw/commons/review/ReviewEntity.kt | 13 + .../free/nrw/commons/review/ReviewHelper.kt | 4 +- .../commons/review/ReviewImageFragment.java | 262 -------------- .../nrw/commons/review/ReviewImageFragment.kt | 251 +++++++++++++ .../commons/review/ReviewPagerAdapter.java | 53 --- .../nrw/commons/review/ReviewPagerAdapter.kt | 37 ++ .../nrw/commons/review/ReviewViewPager.java | 30 -- .../nrw/commons/review/ReviewViewPager.kt | 25 ++ 15 files changed, 906 insertions(+), 932 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt rename app/src/main/java/fr/free/nrw/commons/review/{ReviewDao.java => ReviewDao.kt} (54%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java index 0bd8333e34..9ea59488ed 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java @@ -232,7 +232,7 @@ protected void onProfileClicked() { } protected void onPeerReviewClicked() { - ReviewActivity.startYourself(getActivity(), getString(R.string.title_activity_review)); + ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review)); } } diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java deleted file mode 100644 index 40d743a19a..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java +++ /dev/null @@ -1,334 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.MotionEvent; -import android.view.View; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; -import fr.free.nrw.commons.databinding.ActivityReviewBinding; -import fr.free.nrw.commons.delete.DeleteHelper; -import fr.free.nrw.commons.media.MediaDetailFragment; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Locale; -import javax.inject.Inject; - -public class ReviewActivity extends BaseActivity { - - - private ActivityReviewBinding binding; - - MediaDetailFragment mediaDetailFragment; - public ReviewPagerAdapter reviewPagerAdapter; - public ReviewController reviewController; - @Inject - ReviewHelper reviewHelper; - @Inject - DeleteHelper deleteHelper; - /** - * Represent fragment for ReviewImage - * Use to call some methods of ReviewImage fragment - */ - private ReviewImageFragment reviewImageFragment; - - /** - * Flag to check whether there are any non-hidden categories in the File - */ - private boolean hasNonHiddenCategories = false; - - final String SAVED_MEDIA = "saved_media"; - private Media media; - - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - if (media != null) { - outState.putParcelable(SAVED_MEDIA, media); - } - } - - /** - * Consumers should be simply using this method to use this activity. - * - * @param context - * @param title Page title - */ - public static void startYourself(Context context, String title) { - Intent reviewActivity = new Intent(context, ReviewActivity.class); - reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); - context.startActivity(reviewActivity); - } - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - public Media getMedia() { - return media; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityReviewBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbarBinding.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - - reviewController = new ReviewController(deleteHelper, this); - - reviewPagerAdapter = new ReviewPagerAdapter(getSupportFragmentManager()); - binding.viewPagerReview.setAdapter(reviewPagerAdapter); - binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview); - binding.pbReviewImage.setVisibility(View.VISIBLE); - - Drawable d[]=binding.skipImage.getCompoundDrawablesRelative(); - d[2].setColorFilter(getApplicationContext().getResources().getColor(R.color.button_blue), PorterDuff.Mode.SRC_IN); - - if (savedInstanceState != null && savedInstanceState.getParcelable(SAVED_MEDIA) != null) { - updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)); // Use existing media if we have one - setUpMediaDetailOnOrientation(); - } else { - runRandomizer(); //Run randomizer whenever everything is ready so that a first random image will be added - } - - binding.skipImage.setOnClickListener(view -> { - reviewImageFragment = getInstanceOfReviewImageFragment(); - reviewImageFragment.disableButtons(); - runRandomizer(); - }); - - binding.reviewImageView.setOnClickListener(view ->setUpMediaDetailFragment()); - - binding.skipImage.setOnTouchListener((view, event) -> { - if (event.getAction() == MotionEvent.ACTION_UP && event.getRawX() >= ( - binding.skipImage.getRight() - binding.skipImage - .getCompoundDrawables()[2].getBounds().width())) { - showSkipImageInfo(); - return true; - } - return false; - }); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - @SuppressLint("CheckResult") - public boolean runRandomizer() { - hasNonHiddenCategories = false; - binding.pbReviewImage.setVisibility(View.VISIBLE); - binding.viewPagerReview.setCurrentItem(0); - // Finds non-hidden categories from Media instance - compositeDisposable.add(reviewHelper.getRandomMedia() - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::checkWhetherFileIsUsedInWikis)); - return true; - } - - /** - * Check whether media is used or not in any Wiki Page - */ - @SuppressLint("CheckResult") - private void checkWhetherFileIsUsedInWikis(final Media media) { - compositeDisposable.add(reviewHelper.checkFileUsage(media.getFilename()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - // result false indicates media is not used in any wiki - if (!result) { - // Finds non-hidden categories from Media instance - findNonHiddenCategories(media); - } else { - runRandomizer(); - } - })); - } - - /** - * Finds non-hidden categories and updates current image - */ - private void findNonHiddenCategories(Media media) { - for(String key : media.getCategoriesHiddenStatus().keySet()) { - Boolean value = media.getCategoriesHiddenStatus().get(key); - // If non-hidden category is found then set hasNonHiddenCategories to true - // so that category review cannot be skipped - if(!value) { - hasNonHiddenCategories = true; - break; - } - } - reviewImageFragment = getInstanceOfReviewImageFragment(); - reviewImageFragment.disableButtons(); - updateImage(media); - } - - @SuppressLint("CheckResult") - private void updateImage(Media media) { - reviewHelper.addViewedImagesToDB(media.getPageId()); - this.media = media; - String fileName = media.getFilename(); - if (fileName.length() == 0) { - ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review); - return; - } - - //If The Media User and Current Session Username is same then Skip the Image - if (media.getUser() != null && media.getUser().equals(AccountUtil.getUserName(getApplicationContext()))) { - runRandomizer(); - return; - } - - binding.reviewImageView.setImageURI(media.getImageUrl()); - - reviewController.onImageRefreshed(media); //file name is updated - compositeDisposable.add(reviewHelper.getFirstRevisionOfFile(fileName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(revision -> { - reviewController.firstRevision = revision; - reviewPagerAdapter.updateFileInformation(); - @SuppressLint({"StringFormatInvalid", "LocalSuppress"}) String caption = String.format(getString(R.string.review_is_uploaded_by), fileName, revision.getUser()); - binding.tvImageCaption.setText(caption); - binding.pbReviewImage.setVisibility(View.GONE); - reviewImageFragment = getInstanceOfReviewImageFragment(); - reviewImageFragment.enableButtons(); - })); - binding.viewPagerReview.setCurrentItem(0); - } - - public void swipeToNext() { - int nextPos = binding.viewPagerReview.getCurrentItem() + 1; - // If currently at category fragment, then check whether the media has any non-hidden category - if (nextPos <= 3) { - binding.viewPagerReview.setCurrentItem(nextPos); - if (nextPos == 2) { - // The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually. - if (!hasNonHiddenCategories) { - swipeToNext(); - return; - } - } - } else { - runRandomizer(); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - binding = null; - } - - public void showSkipImageInfo(){ - DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.skip_image).toUpperCase(Locale.ROOT), - getString(R.string.skip_image_explanation), - getString(android.R.string.ok), - "", - null, - null); - } - - public void showReviewImageInfo() { - DialogUtil.showAlertDialog(ReviewActivity.this, - getString(R.string.title_activity_review), - getString(R.string.review_image_explanation), - getString(android.R.string.ok), - "", - null, - null); - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_review_activty, menu); - return super.onCreateOptionsMenu(menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_image_info: - showReviewImageInfo(); - return true; - } - return super.onOptionsItemSelected(item); - } - - /** - * this function return the instance of reviewImageFragment - */ - public ReviewImageFragment getInstanceOfReviewImageFragment(){ - int currentItemOfReviewPager = binding.viewPagerReview.getCurrentItem(); - reviewImageFragment = (ReviewImageFragment) reviewPagerAdapter.instantiateItem(binding.viewPagerReview, currentItemOfReviewPager); - return reviewImageFragment; - } - - /** - * set up the media detail fragment when click on the review image - */ - private void setUpMediaDetailFragment() { - if (binding.mediaDetailContainer.getVisibility() == View.GONE && media != null) { - binding.mediaDetailContainer.setVisibility(View.VISIBLE); - binding.reviewActivityContainer.setVisibility(View.INVISIBLE); - FragmentManager fragmentManager = getSupportFragmentManager(); - mediaDetailFragment = new MediaDetailFragment(); - Bundle bundle = new Bundle(); - bundle.putParcelable("media", media); - mediaDetailFragment.setArguments(bundle); - fragmentManager.beginTransaction().add(R.id.mediaDetailContainer, mediaDetailFragment). - addToBackStack("MediaDetail").commit(); - } - } - - /** - * handle the back pressed event of this activity - * this function call every time when back button is pressed - */ - @Override - public void onBackPressed() { - if (binding.mediaDetailContainer.getVisibility() == View.VISIBLE) { - binding.mediaDetailContainer.setVisibility(View.GONE); - binding.reviewActivityContainer.setVisibility(View.VISIBLE); - } - super.onBackPressed(); - } - - /** - * set up media detail fragment after orientation change - */ - private void setUpMediaDetailOnOrientation() { - Fragment mediaDetailFragment = getSupportFragmentManager() - .findFragmentById(R.id.mediaDetailContainer); - if (mediaDetailFragment != null) { - binding.mediaDetailContainer.setVisibility(View.VISIBLE); - binding.reviewActivityContainer.setVisibility(View.INVISIBLE); - getSupportFragmentManager().beginTransaction() - .replace(R.id.mediaDetailContainer, mediaDetailFragment).commit(); - } - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt new file mode 100644 index 0000000000..44b0f9bc1b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -0,0 +1,336 @@ +package fr.free.nrw.commons.review + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.PorterDuff +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.MotionEvent +import android.view.View +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.AccountUtil +import fr.free.nrw.commons.databinding.ActivityReviewBinding +import fr.free.nrw.commons.delete.DeleteHelper +import fr.free.nrw.commons.media.MediaDetailFragment +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import java.util.Locale +import javax.inject.Inject + +class ReviewActivity : BaseActivity() { + + private lateinit var binding: ActivityReviewBinding + + private var mediaDetailFragment: MediaDetailFragment? = null + lateinit var reviewPagerAdapter: ReviewPagerAdapter + lateinit var reviewController: ReviewController + + @Inject + lateinit var reviewHelper: ReviewHelper + + @Inject + lateinit var deleteHelper: DeleteHelper + + /** + * Represent fragment for ReviewImage + * Use to call some methods of ReviewImage fragment + */ + private var reviewImageFragment: ReviewImageFragment? = null + private var hasNonHiddenCategories = false + var media: Media? = null + + private val SAVED_MEDIA = "saved_media" + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + media?.let { + outState.putParcelable(SAVED_MEDIA, it) + } + } + + /** + * Consumers should be simply using this method to use this activity. + * + * @param context + * @param title Page title + */ + companion object { + fun startYourself(context: Context, title: String) { + val reviewActivity = Intent(context, ReviewActivity::class.java) + reviewActivity.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + reviewActivity.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + context.startActivity(reviewActivity) + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityReviewBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.toolbarBinding?.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + reviewController = ReviewController(deleteHelper, this) + + reviewPagerAdapter = ReviewPagerAdapter(supportFragmentManager) + binding.viewPagerReview.adapter = reviewPagerAdapter + binding.pagerIndicatorReview.setViewPager(binding.viewPagerReview) + binding.pbReviewImage.visibility = View.VISIBLE + + binding.skipImage.compoundDrawablesRelative[2]?.setColorFilter( + resources.getColor(R.color.button_blue), + PorterDuff.Mode.SRC_IN + ) + + if (savedInstanceState?.getParcelable(SAVED_MEDIA) != null) { + updateImage(savedInstanceState.getParcelable(SAVED_MEDIA)!!) + setUpMediaDetailOnOrientation() + } else { + runRandomizer() + } + + binding.skipImage.setOnClickListener { + reviewImageFragment = getInstanceOfReviewImageFragment() + reviewImageFragment?.disableButtons() + runRandomizer() + } + + binding.reviewImageView.setOnClickListener { + setUpMediaDetailFragment() + } + + binding.skipImage.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_UP && + event.rawX >= (binding.skipImage.right - binding.skipImage.compoundDrawables[2].bounds.width()) + ) { + showSkipImageInfo() + true + } else { + false + } + } + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + @SuppressLint("CheckResult") + fun runRandomizer(): Boolean { + hasNonHiddenCategories = false + binding.pbReviewImage.visibility = View.VISIBLE + binding.viewPagerReview.currentItem = 0 + + compositeDisposable.add( + reviewHelper.getRandomMedia() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(::checkWhetherFileIsUsedInWikis) + ) + return true + } + + /** + * Check whether media is used or not in any Wiki Page + */ + @SuppressLint("CheckResult") + private fun checkWhetherFileIsUsedInWikis(media: Media) { + compositeDisposable.add( + reviewHelper.checkFileUsage(media.filename) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { result -> + if (!result) { + findNonHiddenCategories(media) + } else { + runRandomizer() + } + } + ) + } + + /** + * Finds non-hidden categories and updates current image + */ + private fun findNonHiddenCategories(media: Media) { + this.media = media + // If non-hidden category is found then set hasNonHiddenCategories to true + // so that category review cannot be skipped + hasNonHiddenCategories = media.categoriesHiddenStatus.values.any { !it } + reviewImageFragment = getInstanceOfReviewImageFragment() + reviewImageFragment?.disableButtons() + updateImage(media) + } + + @SuppressLint("CheckResult") + private fun updateImage(media: Media) { + reviewHelper.addViewedImagesToDB(media.pageId) + this.media = media + val fileName = media.filename + + if (fileName.isNullOrEmpty()) { + ViewUtil.showShortSnackbar(binding.drawerLayout, R.string.error_review) + return + } + + //If The Media User and Current Session Username is same then Skip the Image + if (media.user == AccountUtil.getUserName(applicationContext)) { + runRandomizer() + return + } + + binding.reviewImageView.setImageURI(media.imageUrl) + + reviewController.onImageRefreshed(media) // filename is updated + compositeDisposable.add( + reviewHelper.getFirstRevisionOfFile(fileName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { revision -> + reviewController.firstRevision = revision + reviewPagerAdapter.updateFileInformation() + val caption = getString( + R.string.review_is_uploaded_by, + fileName, + revision.user + ) + binding.tvImageCaption.text = caption + binding.pbReviewImage.visibility = View.GONE + reviewImageFragment = getInstanceOfReviewImageFragment() + reviewImageFragment?.enableButtons() + } + ) + binding.viewPagerReview.currentItem = 0 + } + + fun swipeToNext() { + val nextPos = binding.viewPagerReview.currentItem + 1 + + // If currently at category fragment, then check whether the media has any non-hidden category + if (nextPos <= 3) { + binding.viewPagerReview.currentItem = nextPos + if (nextPos == 2 && !hasNonHiddenCategories) + { + // The media has no non-hidden category. Such media are already flagged by server-side bots, so no need to review manually. + swipeToNext() + } + } else { + runRandomizer() + } + } + + public override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + fun showSkipImageInfo() { + DialogUtil.showAlertDialog( + this, + getString(R.string.skip_image).uppercase(Locale.ROOT), + getString(R.string.skip_image_explanation), + getString(android.R.string.ok), + null, + null, + null + ) + } + + fun showReviewImageInfo() { + DialogUtil.showAlertDialog( + this, + getString(R.string.title_activity_review), + getString(R.string.review_image_explanation), + getString(android.R.string.ok), + null, + null, + null + ) + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_review_activty, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.menu_image_info -> { + showReviewImageInfo() + true + } + else -> super.onOptionsItemSelected(item) + } + } + + /** + * this function return the instance of reviewImageFragment + */ + private fun getInstanceOfReviewImageFragment(): ReviewImageFragment? { + val currentItemOfReviewPager = binding.viewPagerReview.currentItem + return reviewPagerAdapter.instantiateItem( + binding.viewPagerReview, + currentItemOfReviewPager + ) as? ReviewImageFragment + } + + /** + * set up the media detail fragment when click on the review image + */ + private fun setUpMediaDetailFragment() { + if (binding.mediaDetailContainer.visibility == View.GONE && media != null) { + binding.mediaDetailContainer.visibility = View.VISIBLE + binding.reviewActivityContainer.visibility = View.INVISIBLE + val fragmentManager = supportFragmentManager + mediaDetailFragment = MediaDetailFragment().apply { + arguments = Bundle().apply { + putParcelable("media", media) + } + } + fragmentManager.beginTransaction() + .add(R.id.mediaDetailContainer, mediaDetailFragment!!) + .addToBackStack("MediaDetail") + .commit() + } + } + + /** + * handle the back pressed event of this activity + * this function call every time when back button is pressed + */ + @Deprecated("This method has been deprecated in favor of using the" + + "{@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}." + + "The OnBackPressedDispatcher controls how back button events are dispatched" + + "to one or more {@link OnBackPressedCallback} objects.") + override fun onBackPressed() { + if (binding.mediaDetailContainer.visibility == View.VISIBLE) { + binding.mediaDetailContainer.visibility = View.GONE + binding.reviewActivityContainer.visibility = View.VISIBLE + } + super.onBackPressed() + } + + /** + * set up media detail fragment after orientation change + */ + private fun setUpMediaDetailOnOrientation() { + val fragment = supportFragmentManager.findFragmentById(R.id.mediaDetailContainer) + fragment?.let { + binding.mediaDetailContainer.visibility = View.VISIBLE + binding.reviewActivityContainer.visibility = View.INVISIBLE + supportFragmentManager.beginTransaction() + .replace(R.id.mediaDetailContainer, it) + .commit() + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java deleted file mode 100644 index e3d5b2256e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.java +++ /dev/null @@ -1,220 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.NotificationManager; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.app.NotificationCompat; - -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; - -import java.util.ArrayList; -import java.util.concurrent.Callable; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.actions.PageEditClient; -import fr.free.nrw.commons.actions.ThanksClient; -import fr.free.nrw.commons.delete.DeleteHelper; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -@Singleton -public class ReviewController { - private static final int NOTIFICATION_SEND_THANK = 0x102; - private static final int NOTIFICATION_CHECK_CATEGORY = 0x101; - protected static ArrayList categories; - @Inject - ThanksClient thanksClient; - - @Inject - SessionManager sessionManager; - private final DeleteHelper deleteHelper; - @Nullable - MwQueryPage.Revision firstRevision; // TODO: maybe we can expand this class to include fileName - @Inject - @Named("commons-page-edit") - PageEditClient pageEditClient; - private NotificationManager notificationManager; - private NotificationCompat.Builder notificationBuilder; - private Media media; - - ReviewController(DeleteHelper deleteHelper, Context context) { - this.deleteHelper = deleteHelper; - CommonsApplication.createNotificationChannel(context.getApplicationContext()); - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL); - } - - void onImageRefreshed(Media media) { - this.media = media; - } - - public Media getMedia() { - return media; - } - - public enum DeleteReason { - SPAM, - COPYRIGHT_VIOLATION - } - - void reportSpam(@NonNull Activity activity, ReviewCallback reviewCallback) { - Timber.d("Report spam for %s", media.getFilename()); - deleteHelper.askReasonAndExecute(media, - activity, - activity.getResources().getString(R.string.review_spam_report_question), - DeleteReason.SPAM, - reviewCallback); - } - - void reportPossibleCopyRightViolation(@NonNull Activity activity, ReviewCallback reviewCallback) { - Timber.d("Report spam for %s", media.getFilename()); - deleteHelper.askReasonAndExecute(media, - activity, - activity.getResources().getString(R.string.review_c_violation_report_question), - DeleteReason.COPYRIGHT_VIOLATION, - reviewCallback); - } - - @SuppressLint("CheckResult") - void reportWrongCategory(@NonNull Activity activity, ReviewCallback reviewCallback) { - Context context = activity.getApplicationContext(); - ApplicationlessInjection - .getInstance(context) - .getCommonsApplicationComponent() - .inject(this); - - ViewUtil.showShortToast(context, context.getString(R.string.check_category_toast, media.getDisplayTitle())); - - publishProgress(context, 0); - String summary = context.getString(R.string.check_category_edit_summary); - Observable.defer((Callable>) () -> - pageEditClient.appendEdit(media.getFilename(), "\n{{subst:chc}}\n", summary)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe((result) -> { - publishProgress(context, 2); - String message; - String title; - - if (result) { - title = context.getString(R.string.check_category_success_title); - message = context.getString(R.string.check_category_success_message, media.getDisplayTitle()); - reviewCallback.onSuccess(); - } else { - title = context.getString(R.string.check_category_failure_title); - message = context.getString(R.string.check_category_failure_message, media.getDisplayTitle()); - reviewCallback.onFailure(); - } - - showNotification(title, message); - - }, Timber::e); - } - - private void publishProgress(@NonNull Context context, int i) { - int[] messages = new int[]{R.string.getting_edit_token, R.string.check_category_adding_template}; - String message = ""; - if (0 < i && i < messages.length) { - message = context.getString(messages[i]); - } - - notificationBuilder.setContentTitle(context.getString(R.string.check_category_notification_title, media.getDisplayTitle())) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(messages.length, i, false) - .setOngoing(true); - notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()); - } - - @SuppressLint({"CheckResult", "StringFormatInvalid"}) - void sendThanks(@NonNull Activity activity) { - Context context = activity.getApplicationContext(); - ApplicationlessInjection - .getInstance(context) - .getCommonsApplicationComponent() - .inject(this); - ViewUtil.showShortToast(context, context.getString(R.string.send_thank_toast, media.getDisplayTitle())); - - if (firstRevision == null) { - return; - } - - Observable.defer((Callable>) () -> thanksClient.thank(firstRevision.getRevisionId())) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - displayThanksToast(context, result); - }, throwable -> { - if (throwable instanceof InvalidLoginTokenException) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - activity, - activity.getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - activity, logoutListener); - } else { - Timber.e(throwable); - } - }); - } - - @SuppressLint("StringFormatInvalid") - private void displayThanksToast(final Context context, final boolean result){ - final String message; - final String title; - if (result) { - title = context.getString(R.string.send_thank_success_title); - message = context.getString(R.string.send_thank_success_message, media.getDisplayTitle()); - } else { - title = context.getString(R.string.send_thank_failure_title); - message = context.getString(R.string.send_thank_failure_message, media.getDisplayTitle()); - } - - ViewUtil.showShortToast(context,message); - } - - private void showNotification(String title, String message) { - notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) - .setContentTitle(title) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(message)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(NotificationCompat.PRIORITY_HIGH); - notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()); - } - - public interface ReviewCallback { - void onSuccess(); - - void onFailure(); - - void onTokenException(Exception e); - - void disableButtons(); - - void enableButtons(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt new file mode 100644 index 0000000000..62652bd5bb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewController.kt @@ -0,0 +1,231 @@ +package fr.free.nrw.commons.review + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.NotificationManager +import android.content.Context + +import androidx.core.app.NotificationCompat + +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage + +import java.util.ArrayList +import java.util.concurrent.Callable + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.actions.ThanksClient +import fr.free.nrw.commons.delete.DeleteHelper +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.Observable +import io.reactivex.ObservableSource +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + + +@Singleton +class ReviewController @Inject constructor( + private val deleteHelper: DeleteHelper, + context: Context +) { + + companion object { + private const val NOTIFICATION_SEND_THANK = 0x102 + private const val NOTIFICATION_CHECK_CATEGORY = 0x101 + protected var categories: ArrayList = ArrayList() + } + + @Inject + lateinit var thanksClient: ThanksClient + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + @field: Named("commons-page-edit") + lateinit var pageEditClient: PageEditClient + + var firstRevision: MwQueryPage.Revision? = null // TODO: maybe we can expand this class to include fileName + + private val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val notificationBuilder: NotificationCompat.Builder = + NotificationCompat.Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + + var media: Media? = null + + init { + CommonsApplication.createNotificationChannel(context.applicationContext) + } + + fun onImageRefreshed(media: Media) { + this.media = media + } + + enum class DeleteReason { + SPAM, + COPYRIGHT_VIOLATION + } + + fun reportSpam(activity: Activity, reviewCallback: ReviewCallback) { + Timber.d("Report spam for %s", media?.filename) + deleteHelper.askReasonAndExecute( + media, + activity, + activity.resources.getString(R.string.review_spam_report_question), + DeleteReason.SPAM, + reviewCallback + ) + } + + fun reportPossibleCopyRightViolation(activity: Activity, reviewCallback: ReviewCallback) { + Timber.d("Report copyright violation for %s", media?.filename) + deleteHelper.askReasonAndExecute( + media, + activity, + activity.resources.getString(R.string.review_c_violation_report_question), + DeleteReason.COPYRIGHT_VIOLATION, + reviewCallback + ) + } + + @SuppressLint("CheckResult") + fun reportWrongCategory(activity: Activity, reviewCallback: ReviewCallback) { + val context = activity.applicationContext + ApplicationlessInjection + .getInstance(context) + .commonsApplicationComponent + .inject(this) + + ViewUtil.showShortToast( + context, + context.getString(R.string.check_category_toast, media?.displayTitle) + ) + + publishProgress(context, 0) + val summary = context.getString(R.string.check_category_edit_summary) + + Observable.defer { + pageEditClient.appendEdit(media?.filename ?: "", "\n{{subst:chc}}\n", summary) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + publishProgress(context, 2) + val (title, message) = if (result) { + reviewCallback.onSuccess() + context.getString(R.string.check_category_success_title) to + context.getString(R.string.check_category_success_message, media?.displayTitle) + } else { + reviewCallback.onFailure() + context.getString(R.string.check_category_failure_title) to + context.getString(R.string.check_category_failure_message, media?.displayTitle) + } + showNotification(title, message) + }, Timber::e) + } + + private fun publishProgress(context: Context, progress: Int) { + val messages = arrayOf( + R.string.getting_edit_token, + R.string.check_category_adding_template + ) + + val message = if (progress in 1 until messages.size) { + context.getString(messages[progress]) + } else "" + + notificationBuilder.setContentTitle( + context.getString( + R.string.check_category_notification_title, + media?.displayTitle + ) + ) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(messages.size, progress, false) + .setOngoing(true) + + notificationManager.notify(NOTIFICATION_CHECK_CATEGORY, notificationBuilder.build()) + } + + @SuppressLint("CheckResult") + fun sendThanks(activity: Activity) { + val context = activity.applicationContext + ApplicationlessInjection + .getInstance(context) + .commonsApplicationComponent + .inject(this) + + ViewUtil.showShortToast( + context, + context.getString(R.string.send_thank_toast, media?.displayTitle) + ) + + if (firstRevision == null) return + + Observable.defer { + thanksClient.thank(firstRevision!!.revisionId) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + displayThanksToast(context, result) + }, { throwable -> + if (throwable is InvalidLoginTokenException) { + val username = sessionManager.userName + val logoutListener = CommonsApplication.BaseLogoutListener( + activity, + activity.getString(R.string.invalid_login_message), + username + ) + CommonsApplication.instance.clearApplicationData(activity, logoutListener) + } else { + Timber.e(throwable) + } + }) + } + + @SuppressLint("StringFormatInvalid") + private fun displayThanksToast(context: Context, result: Boolean) { + val (title, message) = if (result) { + context.getString(R.string.send_thank_success_title) to + context.getString(R.string.send_thank_success_message, media?.displayTitle) + } else { + context.getString(R.string.send_thank_failure_title) to + context.getString(R.string.send_thank_failure_message, media?.displayTitle) + } + + ViewUtil.showShortToast(context, message) + } + + private fun showNotification(title: String, message: String) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(title) + .setStyle(NotificationCompat.BigTextStyle().bigText(message)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + notificationManager.notify(NOTIFICATION_SEND_THANK, notificationBuilder.build()) + } + + interface ReviewCallback { + fun onSuccess() + fun onFailure() + fun onTokenException(e: Exception) + fun disableButtons() + fun enableButtons() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt similarity index 54% rename from app/src/main/java/fr/free/nrw/commons/review/ReviewDao.java rename to app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt index c3e8c90a87..1dc9b6ae8b 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.java +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewDao.kt @@ -1,15 +1,15 @@ -package fr.free.nrw.commons.review; +package fr.free.nrw.commons.review -import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; -import androidx.room.Query; +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query /** * Dao interface for reviewed images database */ @Dao -public interface ReviewDao { +interface ReviewDao { /** * Inserts reviewed/skipped image identifier into the database @@ -17,7 +17,7 @@ public interface ReviewDao { * @param reviewEntity */ @Insert(onConflict = OnConflictStrategy.IGNORE) - void insert(ReviewEntity reviewEntity); + fun insert(reviewEntity: ReviewEntity) /** * Checks if the image has already been reviewed/skipped by the user @@ -26,7 +26,6 @@ public interface ReviewDao { * @param imageId * @return */ - @Query( "SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))") - Boolean isReviewedAlready(String imageId); - -} \ No newline at end of file + @Query("SELECT EXISTS (SELECT * from `reviewed-images` where imageId = (:imageId))") + fun isReviewedAlready(imageId: String): Boolean +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java deleted file mode 100644 index 071111b15a..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.java +++ /dev/null @@ -1,19 +0,0 @@ -package fr.free.nrw.commons.review; - -import androidx.annotation.NonNull; -import androidx.room.Entity; -import androidx.room.PrimaryKey; - -/** - * Entity to store reviewed/skipped images identifier - */ -@Entity(tableName = "reviewed-images") -public class ReviewEntity { - @PrimaryKey - @NonNull - String imageId; - - public ReviewEntity(String imageId) { - this.imageId = imageId; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt new file mode 100644 index 0000000000..473c143c72 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewEntity.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.review + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * Entity to store reviewed/skipped images identifier + */ +@Entity(tableName = "reviewed-images") +data class ReviewEntity( + @PrimaryKey + val imageId: String +) diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt index 8a77c11edb..17296a5c85 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewHelper.kt @@ -77,7 +77,7 @@ class ReviewHelper * @param image * @return */ - fun getReviewStatus(image: String?): Boolean = dao?.isReviewedAlready(image) ?: false + fun getReviewStatus(image: String?): Boolean = image?.let { dao?.isReviewedAlready(it) } ?: false /** * Gets the first revision of the file from filename @@ -132,7 +132,7 @@ class ReviewHelper */ fun addViewedImagesToDB(imageId: String?) { Completable - .fromAction { dao!!.insert(ReviewEntity(imageId)) } + .fromAction { imageId?.let { ReviewEntity(it) }?.let { dao!!.insert(it) } } .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java deleted file mode 100644 index 7e0cd0ee3f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.java +++ /dev/null @@ -1,262 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.graphics.Color; -import android.os.Bundle; -import android.text.Html; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.databinding.FragmentReviewImageBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import java.util.ArrayList; -import java.util.List; -import javax.inject.Inject; - -public class ReviewImageFragment extends CommonsDaggerSupportFragment { - - static final int CATEGORY = 2; - private static final int SPAM = 0; - private static final int COPYRIGHT = 1; - private static final int THANKS = 3; - - private int position; - - private FragmentReviewImageBinding binding; - - @Inject - SessionManager sessionManager; - - - // Constant variable used to store user's key name for onSaveInstanceState method - private final String SAVED_USER = "saved_user"; - - // Variable that stores the value of user - private String user; - - public void update(final int position) { - this.position = position; - } - - private String updateCategoriesQuestion() { - final Media media = getReviewActivity().getMedia(); - if (media != null && media.getCategoriesHiddenStatus() != null && isAdded()) { - // Filter category name attribute from all categories - final List categories = new ArrayList<>(); - for(final String key : media.getCategoriesHiddenStatus().keySet()) { - String value = String.valueOf(key); - // Each category returned has a format like "Category:" - // so remove the prefix "Category:" - final int index = key.indexOf("Category:"); - if(index == 0) { - value = key.substring(9); - } - categories.add(value); - } - String catString = TextUtils.join(", ", categories); - if (catString != null && !catString.equals("") && binding.tvReviewQuestionContext != null) { - catString = "" + catString + ""; - final String stringToConvertHtml = String.format(getResources().getString(R.string.review_category_explanation), catString); - return Html.fromHtml(stringToConvertHtml).toString(); - } - } - return getResources().getString(R.string.review_no_category); - } - - @Override - public void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - } - - @Override - public View onCreateView(final LayoutInflater inflater, final ViewGroup container, - final Bundle savedInstanceState) { - position = getArguments().getInt("position"); - binding = FragmentReviewImageBinding.inflate(inflater, container, false); - - final String question; - String explanation=null; - String yesButtonText; - final String noButtonText; - - binding.buttonYes.setOnClickListener(view -> onYesButtonClicked()); - - switch (position) { - case SPAM: - question = getString(R.string.review_spam); - explanation = getString(R.string.review_spam_explanation); - yesButtonText = getString(R.string.yes); - noButtonText = getString(R.string.no); - binding.buttonNo.setOnClickListener(view -> getReviewActivity() - .reviewController.reportSpam(requireActivity(), getReviewCallback())); - break; - case COPYRIGHT: - enableButtons(); - question = getString(R.string.review_copyright); - explanation = getString(R.string.review_copyright_explanation); - yesButtonText = getString(R.string.yes); - noButtonText = getString(R.string.no); - binding.buttonNo.setOnClickListener(view -> getReviewActivity() - .reviewController - .reportPossibleCopyRightViolation(requireActivity(), getReviewCallback())); - break; - case CATEGORY: - enableButtons(); - question = getString(R.string.review_category); - explanation = updateCategoriesQuestion(); - yesButtonText = getString(R.string.yes); - noButtonText = getString(R.string.no); - binding.buttonNo.setOnClickListener(view -> { - getReviewActivity() - .reviewController - .reportWrongCategory(requireActivity(), getReviewCallback()); - getReviewActivity().swipeToNext(); - }); - break; - case THANKS: - enableButtons(); - question = getString(R.string.review_thanks); - - if (getReviewActivity().reviewController.firstRevision != null) { - user = getReviewActivity().reviewController.firstRevision.getUser(); - } else { - if(savedInstanceState != null) { - user = savedInstanceState.getString(SAVED_USER); - } - } - - //if the user is null because of whatsoever reason, review will not be sent anyways - if (!TextUtils.isEmpty(user)) { - explanation = getString(R.string.review_thanks_explanation, user); - } - - // Note that the yes and no buttons are swapped in this section - yesButtonText = getString(R.string.review_thanks_yes_button_text); - noButtonText = getString(R.string.review_thanks_no_button_text); - binding.buttonYes.setTextColor(Color.parseColor("#116aaa")); - binding.buttonNo.setTextColor(Color.parseColor("#228b22")); - binding.buttonNo.setOnClickListener(view -> { - getReviewActivity().reviewController.sendThanks(getReviewActivity()); - getReviewActivity().swipeToNext(); - }); - break; - default: - enableButtons(); - question = "How did we get here?"; - explanation = "No idea."; - yesButtonText = "yes"; - noButtonText = "no"; - } - - binding.tvReviewQuestion.setText(question); - binding.tvReviewQuestionContext.setText(explanation); - binding.buttonYes.setText(yesButtonText); - binding.buttonNo.setText(noButtonText); - return binding.getRoot(); - } - - - /** - * This method will be called when configuration changes happen - * - * @param outState - */ - @Override - public void onSaveInstanceState(@NonNull Bundle outState) { - super.onSaveInstanceState(outState); - - //Save user name when configuration changes happen - outState.putString(SAVED_USER, user); - } - - private ReviewController.ReviewCallback getReviewCallback() { - return new ReviewController - .ReviewCallback() { - @Override - public void onSuccess() { - getReviewActivity().runRandomizer(); - } - - @Override - public void onFailure() { - //do nothing - } - - @Override - public void onTokenException(final Exception e) { - if (e instanceof InvalidLoginTokenException){ - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - getActivity(), - requireActivity().getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - requireActivity(), logoutListener); - - } - } - - /** - * This function is called when an image is being loaded - * to disable the review buttons - */ - @Override - public void disableButtons() { - ReviewImageFragment.this.disableButtons(); - } - - /** - * This function is called when an image has - * been loaded to enable the review buttons. - */ - @Override - public void enableButtons() { - ReviewImageFragment.this.enableButtons(); - } - }; - } - - /** - * This function is called when an image has - * been loaded to enable the review buttons. - */ - public void enableButtons() { - binding.buttonYes.setEnabled(true); - binding.buttonYes.setAlpha(1); - binding.buttonNo.setEnabled(true); - binding.buttonNo.setAlpha(1); - } - - /** - * This function is called when an image is being loaded - * to disable the review buttons - */ - public void disableButtons() { - binding.buttonYes.setEnabled(false); - binding.buttonYes.setAlpha(0.5f); - binding.buttonNo.setEnabled(false); - binding.buttonNo.setAlpha(0.5f); - } - - void onYesButtonClicked() { - getReviewActivity().swipeToNext(); - } - - private ReviewActivity getReviewActivity() { - return (ReviewActivity) requireActivity(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt new file mode 100644 index 0000000000..691c61f569 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewImageFragment.kt @@ -0,0 +1,251 @@ +package fr.free.nrw.commons.review + +import android.graphics.Color +import android.os.Bundle +import android.text.Html +import android.text.TextUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.databinding.FragmentReviewImageBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import java.util.ArrayList +import javax.inject.Inject + + +class ReviewImageFragment : CommonsDaggerSupportFragment() { + + companion object { + const val CATEGORY = 2 + private const val SPAM = 0 + private const val COPYRIGHT = 1 + private const val THANKS = 3 + } + + private var position: Int = 0 + private var binding: FragmentReviewImageBinding? = null + + @Inject + lateinit var sessionManager: SessionManager + + // Constant variable used to store user's key name for onSaveInstanceState method + private val SAVED_USER = "saved_user" + + // Variable that stores the value of user + private var user: String? = null + + fun update(position: Int) { + this.position = position + } + + private fun updateCategoriesQuestion(): String { + val media = reviewActivity.media + if (media?.categoriesHiddenStatus != null && isAdded) { + // Filter category name attribute from all categories + val categories = media.categoriesHiddenStatus.keys.map { key -> + var value = key + // Each category returned has a format like "Category:" + // so remove the prefix "Category:" + if (key.startsWith("Category:")) { + value = key.substring(9) + } + value + } + + val catString = categories.joinToString(", ") + if (catString.isNotEmpty() && binding?.tvReviewQuestionContext != null) { + val formattedCatString = "$catString" + val stringToConvertHtml = getString( + R.string.review_category_explanation, + formattedCatString + ) + return Html.fromHtml(stringToConvertHtml).toString() + } + } + return getString(R.string.review_no_category) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + position = requireArguments().getInt("position") + binding = FragmentReviewImageBinding.inflate(inflater, container, false) + + val question: String + var explanation: String? = null + val yesButtonText: String + val noButtonText: String + + binding?.buttonYes?.setOnClickListener { onYesButtonClicked() } + + when (position) { + SPAM -> { + question = getString(R.string.review_spam) + explanation = getString(R.string.review_spam_explanation) + yesButtonText = getString(R.string.yes) + noButtonText = getString(R.string.no) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.reportSpam(requireActivity(), reviewCallback) + } + } + COPYRIGHT -> { + enableButtons() + question = getString(R.string.review_copyright) + explanation = getString(R.string.review_copyright_explanation) + yesButtonText = getString(R.string.yes) + noButtonText = getString(R.string.no) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.reportPossibleCopyRightViolation( + requireActivity(), + reviewCallback + ) + } + } + CATEGORY -> { + enableButtons() + question = getString(R.string.review_category) + explanation = updateCategoriesQuestion() + yesButtonText = getString(R.string.yes) + noButtonText = getString(R.string.no) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.reportWrongCategory( + requireActivity(), + reviewCallback + ) + reviewActivity.swipeToNext() + } + } + THANKS -> { + enableButtons() + question = getString(R.string.review_thanks) + + user = reviewActivity.reviewController.firstRevision?.user + ?: savedInstanceState?.getString(SAVED_USER) + + //if the user is null because of whatsoever reason, review will not be sent anyways + if (!user.isNullOrEmpty()) { + explanation = getString(R.string.review_thanks_explanation, user) + } + + // Note that the yes and no buttons are swapped in this section + yesButtonText = getString(R.string.review_thanks_yes_button_text) + noButtonText = getString(R.string.review_thanks_no_button_text) + binding?.buttonYes?.setTextColor(Color.parseColor("#116aaa")) + binding?.buttonNo?.setTextColor(Color.parseColor("#228b22")) + binding?.buttonNo?.setOnClickListener { + reviewActivity.reviewController.sendThanks(requireActivity()) + reviewActivity.swipeToNext() + } + } + else -> { + enableButtons() + question = "How did we get here?" + explanation = "No idea." + yesButtonText = "yes" + noButtonText = "no" + } + } + + binding?.apply { + tvReviewQuestion.text = question + tvReviewQuestionContext.text = explanation + buttonYes.text = yesButtonText + buttonNo.text = noButtonText + } + return binding?.root + } + + /** + * This method will be called when configuration changes happen + * + * @param outState + */ + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + //Save user name when configuration changes happen + outState.putString(SAVED_USER, user) + } + + private val reviewCallback: ReviewController.ReviewCallback + get() = object : ReviewController.ReviewCallback { + override fun onSuccess() { + reviewActivity.runRandomizer() + } + + override fun onFailure() { + //do nothing + } + + override fun onTokenException(e: Exception) { + if (e is InvalidLoginTokenException) { + val username = sessionManager.userName + val logoutListener = activity?.let { + CommonsApplication.BaseLogoutListener( + it, + getString(R.string.invalid_login_message), + username + ) + } + + if (logoutListener != null) { + CommonsApplication.instance.clearApplicationData( + requireActivity(), logoutListener + ) + } + } + } + + override fun disableButtons() { + this@ReviewImageFragment.disableButtons() + } + + override fun enableButtons() { + this@ReviewImageFragment.enableButtons() + } + } + + /** + * This function is called when an image has + * been loaded to enable the review buttons. + */ + fun enableButtons() { + binding?.apply { + buttonYes.isEnabled = true + buttonYes.alpha = 1f + buttonNo.isEnabled = true + buttonNo.alpha = 1f + } + } + + /** + * This function is called when an image is being loaded + * to disable the review buttons + */ + fun disableButtons() { + binding?.apply { + buttonYes.isEnabled = false + buttonYes.alpha = 0.5f + buttonNo.isEnabled = false + buttonNo.alpha = 0.5f + } + } + + fun onYesButtonClicked() { + reviewActivity.swipeToNext() + } + + private val reviewActivity: ReviewActivity + get() = requireActivity() as ReviewActivity + + override fun onDestroy() { + super.onDestroy() + binding = null + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java deleted file mode 100644 index 16b55c6e9a..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.java +++ /dev/null @@ -1,53 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.os.Bundle; - -import android.view.ViewGroup; -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentStatePagerAdapter; - -public class ReviewPagerAdapter extends FragmentStatePagerAdapter { - private ReviewImageFragment[] reviewImageFragments; - - /** - * this function return the instance of ReviewviewPage current item - */ - @Override - public Object instantiateItem(@NonNull ViewGroup container, int position) { - return super.instantiateItem(container, position); - } - - ReviewPagerAdapter(FragmentManager fm) { - super(fm); - reviewImageFragments = new ReviewImageFragment[]{ - new ReviewImageFragment(), - new ReviewImageFragment(), - new ReviewImageFragment(), - new ReviewImageFragment() - }; - } - - @Override - public int getCount() { - return reviewImageFragments.length; - } - - void updateFileInformation() { - for (int i = 0; i < getCount(); i++) { - ReviewImageFragment fragment = reviewImageFragments[i]; - fragment.update(i); - } - } - - - @Override - public Fragment getItem(int position) { - Bundle bundle = new Bundle(); - bundle.putInt("position", position); - reviewImageFragments[position].setArguments(bundle); - return reviewImageFragments[position]; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt new file mode 100644 index 0000000000..9bbe14e65a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewPagerAdapter.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.review + +import android.os.Bundle + +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter + + +class ReviewPagerAdapter(fm: FragmentManager) : FragmentStatePagerAdapter(fm) { + private val reviewImageFragments: Array = arrayOf( + ReviewImageFragment(), + ReviewImageFragment(), + ReviewImageFragment(), + ReviewImageFragment() + ) + + override fun getCount(): Int { + return reviewImageFragments.size + } + + fun updateFileInformation() { + for (i in 0 until count) { + val fragment = reviewImageFragments[i] + fragment.update(i) + } + } + + override fun getItem(position: Int): Fragment { + val bundle = Bundle().apply { + putInt("position", position) + } + reviewImageFragments[position].arguments = bundle + return reviewImageFragments[position] + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java deleted file mode 100644 index 95740aac0f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.java +++ /dev/null @@ -1,30 +0,0 @@ -package fr.free.nrw.commons.review; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; - -import androidx.viewpager.widget.ViewPager; - -public class ReviewViewPager extends ViewPager { - - public ReviewViewPager(Context context) { - super(context); - } - - public ReviewViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt new file mode 100644 index 0000000000..39de49189b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewViewPager.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.review + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent + +import androidx.viewpager.widget.ViewPager + +class ReviewViewPager @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : ViewPager(context, attrs) { + + override fun onInterceptTouchEvent(event: MotionEvent): Boolean { + // Never allow swiping to switch between pages + return false + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + // Never allow swiping to switch between pages + return false + } +} From 00cfd835213b4f2216126ccbac762ae8506415b6 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Sun, 24 Nov 2024 15:47:05 +0530 Subject: [PATCH 033/231] Migrated quiz module from Java to Kotlin (#5952) * Rename .java to .kt * Migrated quiz module to Kotlin * unit test failing fixed * unit test failing fixed --- .../free/nrw/commons/quiz/QuizActivity.java | 146 ------------- .../fr/free/nrw/commons/quiz/QuizActivity.kt | 154 ++++++++++++++ .../fr/free/nrw/commons/quiz/QuizChecker.java | 167 --------------- .../fr/free/nrw/commons/quiz/QuizChecker.kt | 175 ++++++++++++++++ .../free/nrw/commons/quiz/QuizController.java | 63 ------ .../free/nrw/commons/quiz/QuizController.kt | 76 +++++++ .../nrw/commons/quiz/QuizResultActivity.java | 188 ----------------- .../nrw/commons/quiz/QuizResultActivity.kt | 192 ++++++++++++++++++ .../nrw/commons/quiz/RadioGroupHelper.java | 64 ------ .../free/nrw/commons/quiz/RadioGroupHelper.kt | 61 ++++++ 10 files changed, 658 insertions(+), 628 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java deleted file mode 100644 index 8c087b17b5..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.java +++ /dev/null @@ -1,146 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.content.Intent; -import android.os.Bundle; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; - -import com.facebook.drawee.drawable.ProgressBarDrawable; -import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; - -import fr.free.nrw.commons.databinding.ActivityQuizBinding; -import java.util.ArrayList; - -import fr.free.nrw.commons.R; - -public class QuizActivity extends AppCompatActivity { - - private ActivityQuizBinding binding; - private final QuizController quizController = new QuizController(); - private ArrayList quiz = new ArrayList<>(); - private int questionIndex = 0; - private int score; - /** - * isPositiveAnswerChecked : represents yes click event - */ - private boolean isPositiveAnswerChecked; - /** - * isNegativeAnswerChecked : represents no click event - */ - private boolean isNegativeAnswerChecked; - - @Override - protected void onCreate(final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityQuizBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - quizController.initialize(this); - setSupportActionBar(binding.toolbar.toolbar); - binding.nextButton.setOnClickListener(view -> notKnowAnswer()); - displayQuestion(); - } - - /** - * to move to next question and check whether answer is selected or not - */ - public void setNextQuestion(){ - if ( questionIndex <= quiz.size() && (isPositiveAnswerChecked || isNegativeAnswerChecked)) { - evaluateScore(); - } - } - - public void notKnowAnswer(){ - customAlert("Information", quiz.get(questionIndex).getAnswerMessage()); - } - - /** - * to give warning before ending quiz - */ - @Override - public void onBackPressed() { - new AlertDialog.Builder(this) - .setTitle(getResources().getString(R.string.warning)) - .setMessage(getResources().getString(R.string.quiz_back_button)) - .setPositiveButton(R.string.continue_message, (dialog, which) -> { - final Intent intent = new Intent(this, QuizResultActivity.class); - dialog.dismiss(); - intent.putExtra("QuizResult", score); - startActivity(intent); - }) - .setNegativeButton("Cancel", (dialogInterface, i) -> dialogInterface.dismiss()) - .create() - .show(); - } - - /** - * to display the question - */ - public void displayQuestion() { - quiz = quizController.getQuiz(); - binding.question.questionText.setText(quiz.get(questionIndex).getQuestion()); - binding.questionTitle.setText( - getResources().getString(R.string.question) + - quiz.get(questionIndex).getQuestionNumber() - ); - binding.question.questionImage.setHierarchy(GenericDraweeHierarchyBuilder - .newInstance(getResources()) - .setFailureImage(VectorDrawableCompat.create(getResources(), - R.drawable.ic_error_outline_black_24dp, getTheme())) - .setProgressBarImage(new ProgressBarDrawable()) - .build()); - - binding.question.questionImage.setImageURI(quiz.get(questionIndex).getUrl()); - isPositiveAnswerChecked = false; - isNegativeAnswerChecked = false; - binding.answer.quizPositiveAnswer.setOnClickListener(view -> { - isPositiveAnswerChecked = true; - setNextQuestion(); - }); - binding.answer.quizNegativeAnswer.setOnClickListener(view -> { - isNegativeAnswerChecked = true; - setNextQuestion(); - }); - } - - /** - * to evaluate score and check whether answer is correct or wrong - */ - public void evaluateScore() { - if ((quiz.get(questionIndex).isAnswer() && isPositiveAnswerChecked) || - (!quiz.get(questionIndex).isAnswer() && isNegativeAnswerChecked) ){ - customAlert(getResources().getString(R.string.correct), - quiz.get(questionIndex).getAnswerMessage()); - score++; - } else { - customAlert(getResources().getString(R.string.wrong), - quiz.get(questionIndex).getAnswerMessage()); - } - } - - /** - * to display explanation after each answer, update questionIndex and move to next question - * @param title the alert title - * @param Message the alert message - */ - public void customAlert(final String title, final String Message) { - new AlertDialog.Builder(this) - .setTitle(title) - .setMessage(Message) - .setPositiveButton(R.string.continue_message, (dialog, which) -> { - questionIndex++; - if (questionIndex == quiz.size()) { - final Intent intent = new Intent(this, QuizResultActivity.class); - dialog.dismiss(); - intent.putExtra("QuizResult", score); - startActivity(intent); - } else { - displayQuestion(); - } - }) - .create() - .show(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt new file mode 100644 index 0000000000..a243c2637e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizActivity.kt @@ -0,0 +1,154 @@ +package fr.free.nrw.commons.quiz + +import android.annotation.SuppressLint +import android.content.Intent +import android.os.Bundle + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat + +import com.facebook.drawee.drawable.ProgressBarDrawable +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder + +import fr.free.nrw.commons.databinding.ActivityQuizBinding +import java.util.ArrayList + +import fr.free.nrw.commons.R + + +class QuizActivity : AppCompatActivity() { + + private lateinit var binding: ActivityQuizBinding + private val quizController = QuizController() + private var quiz = ArrayList() + private var questionIndex = 0 + private var score = 0 + + /** + * isPositiveAnswerChecked : represents yes click event + */ + private var isPositiveAnswerChecked = false + + /** + * isNegativeAnswerChecked : represents no click event + */ + private var isNegativeAnswerChecked = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityQuizBinding.inflate(layoutInflater) + setContentView(binding.root) + + quizController.initialize(this) + setSupportActionBar(binding.toolbar.toolbar) + binding.nextButton.setOnClickListener { notKnowAnswer() } + displayQuestion() + } + + /** + * To move to next question and check whether answer is selected or not + */ + fun setNextQuestion() { + if (questionIndex <= quiz.size && (isPositiveAnswerChecked || isNegativeAnswerChecked)) { + evaluateScore() + } + } + + private fun notKnowAnswer() { + customAlert("Information", quiz[questionIndex].answerMessage) + } + + /** + * To give warning before ending quiz + */ + override fun onBackPressed() { + AlertDialog.Builder(this) + .setTitle(getString(R.string.warning)) + .setMessage(getString(R.string.quiz_back_button)) + .setPositiveButton(R.string.continue_message) { dialog, _ -> + val intent = Intent(this, QuizResultActivity::class.java) + dialog.dismiss() + intent.putExtra("QuizResult", score) + startActivity(intent) + } + .setNegativeButton("Cancel") { dialogInterface, _ -> dialogInterface.dismiss() } + .create() + .show() + } + + /** + * To display the question + */ + @SuppressLint("SetTextI18n") + private fun displayQuestion() { + quiz = quizController.getQuiz() + binding.question.questionText.text = quiz[questionIndex].question + binding.questionTitle.text = getString(R.string.question) + quiz[questionIndex].questionNumber + + binding.question.questionImage.hierarchy = GenericDraweeHierarchyBuilder + .newInstance(resources) + .setFailureImage(VectorDrawableCompat.create(resources, R.drawable.ic_error_outline_black_24dp, theme)) + .setProgressBarImage(ProgressBarDrawable()) + .build() + + binding.question.questionImage.setImageURI(quiz[questionIndex].getUrl()) + isPositiveAnswerChecked = false + isNegativeAnswerChecked = false + + binding.answer.quizPositiveAnswer.setOnClickListener { + isPositiveAnswerChecked = true + setNextQuestion() + } + binding.answer.quizNegativeAnswer.setOnClickListener { + isNegativeAnswerChecked = true + setNextQuestion() + } + } + + /** + * To evaluate score and check whether answer is correct or wrong + */ + fun evaluateScore() { + if ( + (quiz[questionIndex].isAnswer && isPositiveAnswerChecked) + || + (!quiz[questionIndex].isAnswer && isNegativeAnswerChecked) + ) { + customAlert( + getString(R.string.correct), + quiz[questionIndex].answerMessage + ) + score++ + } else { + customAlert( + getString(R.string.wrong), + quiz[questionIndex].answerMessage + ) + } + } + + /** + * To display explanation after each answer, update questionIndex and move to next question + * @param title The alert title + * @param message The alert message + */ + fun customAlert(title: String, message: String) { + AlertDialog.Builder(this) + .setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.continue_message) { dialog, _ -> + questionIndex++ + if (questionIndex == quiz.size) { + val intent = Intent(this, QuizResultActivity::class.java) + dialog.dismiss() + intent.putExtra("QuizResult", score) + startActivity(intent) + } else { + displayQuestion() + } + } + .create() + .show() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java deleted file mode 100644 index 201c5bfc68..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.java +++ /dev/null @@ -1,167 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Intent; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.DialogUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import timber.log.Timber; - -/** - * fetches the number of images uploaded and number of images reverted. - * Then it calculates the percentage of the images reverted - * if the percentage of images reverted after last quiz exceeds 50% and number of images uploaded is - * greater than 50, then quiz is popped up - */ -@Singleton -public class QuizChecker { - - private int revertCount ; - private int totalUploadCount ; - private boolean isRevertCountFetched; - private boolean isUploadCountFetched; - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - private final SessionManager sessionManager; - private final OkHttpJsonApiClient okHttpJsonApiClient; - private final JsonKvStore revertKvStore; - - private static final int UPLOAD_COUNT_THRESHOLD = 5; - private static final String REVERT_PERCENTAGE_FOR_MESSAGE = "50%"; - private final String REVERT_SHARED_PREFERENCE = "revertCount"; - private final String UPLOAD_SHARED_PREFERENCE = "uploadCount"; - - /** - * constructor to set the parameters for quiz - * @param sessionManager - * @param okHttpJsonApiClient - */ - @Inject - public QuizChecker(SessionManager sessionManager, - OkHttpJsonApiClient okHttpJsonApiClient, - @Named("default_preferences") JsonKvStore revertKvStore) { - this.sessionManager = sessionManager; - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.revertKvStore = revertKvStore; - } - - public void initQuizCheck(Activity activity) { - calculateRevertParameterAndShowQuiz(activity); - } - - public void cleanup() { - compositeDisposable.clear(); - } - - /** - * to fet the total number of images uploaded - */ - private void setUploadCount() { - compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(sessionManager.getUserName()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::setTotalUploadCount, - t -> Timber.e(t, "Fetching upload count failed") - )); - } - - /** - * set the sub Title of Contibutions Activity and - * call function to check for quiz - * @param uploadCount user's upload count - */ - private void setTotalUploadCount(int uploadCount) { - totalUploadCount = uploadCount - revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0); - if ( totalUploadCount < 0){ - totalUploadCount = 0; - revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0); - } - isUploadCountFetched = true; - } - - /** - * To call the API to get reverts count in form of JSONObject - */ - private void setRevertCount() { - compositeDisposable.add(okHttpJsonApiClient - .getAchievements(sessionManager.getUserName()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setRevertParameter(response.getDeletedUploads()); - } - }, throwable -> Timber.e(throwable, "Fetching feedback failed")) - ); - } - - /** - * to calculate the number of images reverted after previous quiz - * @param revertCountFetched count of deleted uploads - */ - private void setRevertParameter(int revertCountFetched) { - revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0); - if (revertCount < 0){ - revertCount = 0; - revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0); - } - isRevertCountFetched = true; - } - - /** - * to check whether the criterion to call quiz is satisfied - */ - private void calculateRevertParameterAndShowQuiz(Activity activity) { - setUploadCount(); - setRevertCount(); - if ( revertCount < 0 || totalUploadCount < 0){ - revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0); - revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0); - return; - } - if (isRevertCountFetched && isUploadCountFetched && - totalUploadCount >= UPLOAD_COUNT_THRESHOLD && - (revertCount * 100) / totalUploadCount >= 50) { - callQuiz(activity); - } - } - - /** - * Alert which prompts to quiz - */ - @SuppressLint("StringFormatInvalid") - private void callQuiz(Activity activity) { - DialogUtil.showAlertDialog(activity, - activity.getString(R.string.quiz), - activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE), - activity.getString(R.string.about_translate_proceed), - activity.getString(android.R.string.cancel), - () -> startQuizActivity(activity), - null); - } - - private void startQuizActivity(Activity activity) { - int newRevetSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0); - revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevetSharedPrefs); - int newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0); - revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount); - Intent i = new Intent(activity, WelcomeActivity.class); - i.putExtra("isQuiz", true); - activity.startActivity(i); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt new file mode 100644 index 0000000000..ec74ecf6f3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizChecker.kt @@ -0,0 +1,175 @@ +package fr.free.nrw.commons.quiz + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent + +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.WelcomeActivity +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.utils.DialogUtil +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber + + +/** + * Fetches the number of images uploaded and number of images reverted. + * Then it calculates the percentage of the images reverted. + * If the percentage of images reverted after the last quiz exceeds 50% and number of images uploaded is + * greater than 50, then the quiz is popped up. + */ +@Singleton +class QuizChecker @Inject constructor( + private val sessionManager: SessionManager, + private val okHttpJsonApiClient: OkHttpJsonApiClient, + @Named("default_preferences") private val revertKvStore: JsonKvStore +) { + + private var revertCount = 0 + private var totalUploadCount = 0 + private var isRevertCountFetched = false + private var isUploadCountFetched = false + + private val compositeDisposable = CompositeDisposable() + + private val UPLOAD_COUNT_THRESHOLD = 5 + private val REVERT_PERCENTAGE_FOR_MESSAGE = "50%" + private val REVERT_SHARED_PREFERENCE = "revertCount" + private val UPLOAD_SHARED_PREFERENCE = "uploadCount" + + /** + * Initializes quiz check by calculating revert parameters and showing quiz if necessary + */ + fun initQuizCheck(activity: Activity) { + calculateRevertParameterAndShowQuiz(activity) + } + + /** + * Clears disposables to avoid memory leaks + */ + fun cleanup() { + compositeDisposable.clear() + } + + /** + * Fetches the total number of images uploaded + */ + private fun setUploadCount() { + compositeDisposable.add( + okHttpJsonApiClient.getUploadCount(sessionManager.userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { uploadCount -> setTotalUploadCount(uploadCount) }, + { t -> Timber.e(t, "Fetching upload count failed") } + ) + ) + } + + /** + * Sets the total upload count after subtracting stored preference + * @param uploadCount User's upload count + */ + private fun setTotalUploadCount(uploadCount: Int) { + totalUploadCount = uploadCount - revertKvStore.getInt( + UPLOAD_SHARED_PREFERENCE, + 0 + ) + if (totalUploadCount < 0) { + totalUploadCount = 0 + revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0) + } + isUploadCountFetched = true + } + + /** + * Fetches the revert count using the API + */ + private fun setRevertCount() { + compositeDisposable.add( + okHttpJsonApiClient.getAchievements(sessionManager.userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + response?.let { setRevertParameter(it.deletedUploads) } + }, + { throwable -> Timber.e(throwable, "Fetching feedback failed") } + ) + ) + } + + /** + * Calculates the number of images reverted after the previous quiz + * @param revertCountFetched Count of deleted uploads + */ + private fun setRevertParameter(revertCountFetched: Int) { + revertCount = revertCountFetched - revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0) + if (revertCount < 0) { + revertCount = 0 + revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0) + } + isRevertCountFetched = true + } + + /** + * Checks whether the criteria for calling the quiz are satisfied + */ + private fun calculateRevertParameterAndShowQuiz(activity: Activity) { + setUploadCount() + setRevertCount() + + if (revertCount < 0 || totalUploadCount < 0) { + revertKvStore.putInt(REVERT_SHARED_PREFERENCE, 0) + revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, 0) + return + } + + if (isRevertCountFetched && isUploadCountFetched && + totalUploadCount >= UPLOAD_COUNT_THRESHOLD && + (revertCount * 100) / totalUploadCount >= 50 + ) { + callQuiz(activity) + } + } + + /** + * Displays an alert prompting the user to take the quiz + */ + @SuppressLint("StringFormatInvalid") + private fun callQuiz(activity: Activity) { + DialogUtil.showAlertDialog( + activity, + activity.getString(R.string.quiz), + activity.getString(R.string.quiz_alert_message, REVERT_PERCENTAGE_FOR_MESSAGE), + activity.getString(R.string.about_translate_proceed), + activity.getString(android.R.string.cancel), + { startQuizActivity(activity) }, + null + ) + } + + /** + * Starts the quiz activity and updates preferences for revert and upload counts + */ + private fun startQuizActivity(activity: Activity) { + val newRevertSharedPrefs = revertCount + revertKvStore.getInt(REVERT_SHARED_PREFERENCE, 0) + revertKvStore.putInt(REVERT_SHARED_PREFERENCE, newRevertSharedPrefs) + + val newUploadCount = totalUploadCount + revertKvStore.getInt(UPLOAD_SHARED_PREFERENCE, 0) + revertKvStore.putInt(UPLOAD_SHARED_PREFERENCE, newUploadCount) + + val intent = Intent(activity, WelcomeActivity::class.java).apply { + putExtra("isQuiz", true) + } + activity.startActivity(intent) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java deleted file mode 100644 index a7b2c94ef2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.content.Context; - -import java.util.ArrayList; - -import fr.free.nrw.commons.R; - -/** - * controls the quiz in the Activity - */ -public class QuizController { - - ArrayList quiz = new ArrayList<>(); - - private final String URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg"; - private final String URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg"; - private final String URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg"; - private final String URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png"; - private final String URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg"; - - public void initialize(Context context){ - QuizQuestion q1 = new QuizQuestion(1, - context.getString(R.string.quiz_question_string), - URL_FOR_SELFIE, - false, - context.getString(R.string.selfie_answer)); - quiz.add(q1); - - QuizQuestion q2 = new QuizQuestion(2, - context.getString(R.string.quiz_question_string), - URL_FOR_TAJ_MAHAL, - true, - context.getString(R.string.taj_mahal_answer)); - quiz.add(q2); - - QuizQuestion q3 = new QuizQuestion(3, - context.getString(R.string.quiz_question_string), - URL_FOR_BLURRY_IMAGE, - false, - context.getString(R.string.blurry_image_answer)); - quiz.add(q3); - - QuizQuestion q4 = new QuizQuestion(4, - context.getString(R.string.quiz_screenshot_question), - URL_FOR_SCREENSHOT, - false, - context.getString(R.string.screenshot_answer)); - quiz.add(q4); - - QuizQuestion q5 = new QuizQuestion(5, - context.getString(R.string.quiz_question_string), - URL_FOR_EVENT, - true, - context.getString(R.string.construction_event_answer)); - quiz.add(q5); - - } - - public ArrayList getQuiz() { - return quiz; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt new file mode 100644 index 0000000000..3cb4f52a67 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizController.kt @@ -0,0 +1,76 @@ +package fr.free.nrw.commons.quiz + +import android.content.Context + +import java.util.ArrayList + +import fr.free.nrw.commons.R + + +/** + * Controls the quiz in the Activity + */ +class QuizController { + + private val quiz: ArrayList = ArrayList() + + private val URL_FOR_SELFIE = "https://i.imgur.com/0fMYcpM.jpg" + private val URL_FOR_TAJ_MAHAL = "https://upload.wikimedia.org/wikipedia/commons/1/15/Taj_Mahal-03.jpg" + private val URL_FOR_BLURRY_IMAGE = "https://i.imgur.com/Kepb5jR.jpg" + private val URL_FOR_SCREENSHOT = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8b/Social_media_app_mockup_screenshot.svg/500px-Social_media_app_mockup_screenshot.svg.png" + private val URL_FOR_EVENT = "https://upload.wikimedia.org/wikipedia/commons/5/51/HouseBuildingInNorthernVietnam.jpg" + + fun initialize(context: Context) { + val q1 = QuizQuestion( + 1, + context.getString(R.string.quiz_question_string), + URL_FOR_SELFIE, + false, + context.getString(R.string.selfie_answer) + ) + quiz.add(q1) + + val q2 = QuizQuestion( + 2, + context.getString(R.string.quiz_question_string), + URL_FOR_TAJ_MAHAL, + true, + context.getString(R.string.taj_mahal_answer) + ) + quiz.add(q2) + + val q3 = QuizQuestion( + 3, + context.getString(R.string.quiz_question_string), + URL_FOR_BLURRY_IMAGE, + false, + context.getString(R.string.blurry_image_answer) + ) + quiz.add(q3) + + val q4 = QuizQuestion( + 4, + context.getString(R.string.quiz_screenshot_question), + URL_FOR_SCREENSHOT, + false, + context.getString(R.string.screenshot_answer) + ) + quiz.add(q4) + + val q5 = QuizQuestion( + 5, + context.getString(R.string.quiz_question_string), + URL_FOR_EVENT, + true, + context.getString(R.string.construction_event_answer) + ) + quiz.add(q5) + } + + fun getQuiz(): ArrayList { + return quiz + } +} + + + diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java deleted file mode 100644 index ec6d1070d0..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.java +++ /dev/null @@ -1,188 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; - -import fr.free.nrw.commons.databinding.ActivityQuizResultBinding; -import java.io.File; -import java.io.FileOutputStream; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.MainActivity; - - -/** - * Displays the final score of quiz and congratulates the user - */ -public class QuizResultActivity extends AppCompatActivity { - - private ActivityQuizResultBinding binding; - private final int NUMBER_OF_QUESTIONS = 5; - private final int MULTIPLIER_TO_GET_PERCENTAGE = 20; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - binding = ActivityQuizResultBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - setSupportActionBar(binding.toolbar.toolbar); - - binding.quizResultNext.setOnClickListener(view -> launchContributionActivity()); - - if ( getIntent() != null) { - Bundle extras = getIntent().getExtras(); - int score = extras.getInt("QuizResult"); - setScore(score); - }else{ - startActivityWithFlags( - this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - super.onBackPressed(); - } - } - - @Override - protected void onDestroy() { - binding = null; - super.onDestroy(); - } - - /** - * to calculate and display percentage and score - * @param score - */ - public void setScore(int score) { - final int scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE; - binding.resultProgressBar.setProgress(scorePercent); - binding.tvResultProgress.setText(score +" / " + NUMBER_OF_QUESTIONS); - final String message = getResources().getString(R.string.congratulatory_message_quiz,scorePercent + "%"); - binding.congratulatoryMessage.setText(message); - } - - /** - * to go to Contibutions Activity - */ - public void launchContributionActivity(){ - startActivityWithFlags( - this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - } - - @Override - public void onBackPressed() { - startActivityWithFlags( - this, MainActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP, - Intent.FLAG_ACTIVITY_SINGLE_TOP); - super.onBackPressed(); - } - - /** - * Function to call intent to an activity - * @param context - * @param cls - * @param flags - * @param - */ - public static void startActivityWithFlags(Context context, Class cls, int... flags) { - Intent intent = new Intent(context, cls); - for (int flag: flags) { - intent.addFlags(flag); - } - context.startActivity(intent); - } - - /** - * to inflate menu - * @param menu - * @return - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_about, menu); - return true; - } - - /** - * if share option selected then take screenshot and launch alert - * @param item - * @return - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int id = item.getItemId(); - if (id == R.id.share_app_icon) { - View rootView = getWindow().getDecorView().findViewById(android.R.id.content); - Bitmap screenShot = getScreenShot(rootView); - showAlert(screenShot); - } - - return super.onOptionsItemSelected(item); - } - - /** - * to store the screenshot of image in bitmap variable temporarily - * @param view - * @return - */ - public static Bitmap getScreenShot(View view) { - View screenView = view.getRootView(); - screenView.setDrawingCacheEnabled(true); - Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache()); - screenView.setDrawingCacheEnabled(false); - return bitmap; - } - - /** - * share the screenshot through social media - * @param bitmap - */ - void shareScreen(Bitmap bitmap) { - try { - File file = new File(this.getExternalCacheDir(),"screen.png"); - FileOutputStream fOut = new FileOutputStream(file); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); - fOut.flush(); - fOut.close(); - file.setReadable(true, false); - final Intent intent = new Intent(android.content.Intent.ACTION_SEND); - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); - intent.setType("image/png"); - startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * It display the alertDialog with Image of screenshot - * @param screenshot - */ - public void showAlert(Bitmap screenshot) { - AlertDialog.Builder alertadd = new AlertDialog.Builder(QuizResultActivity.this); - LayoutInflater factory = LayoutInflater.from(QuizResultActivity.this); - final View view = factory.inflate(R.layout.image_alert_layout, null); - ImageView screenShotImage = view.findViewById(R.id.alert_image); - screenShotImage.setImageBitmap(screenshot); - TextView shareMessage = view.findViewById(R.id.alert_text); - shareMessage.setText(R.string.quiz_result_share_message); - alertadd.setView(view); - alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot)); - alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel()); - alertadd.show(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt new file mode 100644 index 0000000000..1d4821ee3d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/QuizResultActivity.kt @@ -0,0 +1,192 @@ +package fr.free.nrw.commons.quiz + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.TextView + +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity + +import fr.free.nrw.commons.databinding.ActivityQuizResultBinding +import java.io.File +import java.io.FileOutputStream + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.MainActivity + + +/** + * Displays the final score of quiz and congratulates the user + */ +class QuizResultActivity : AppCompatActivity() { + + private var binding: ActivityQuizResultBinding? = null + private val NUMBER_OF_QUESTIONS = 5 + private val MULTIPLIER_TO_GET_PERCENTAGE = 20 + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityQuizResultBinding.inflate(layoutInflater) + setContentView(binding?.root) + + setSupportActionBar(binding?.toolbar?.toolbar) + + binding?.quizResultNext?.setOnClickListener { + launchContributionActivity() + } + + intent?.extras?.let { extras -> + val score = extras.getInt("QuizResult", 0) + setScore(score) + } ?: run { + startActivityWithFlags( + this, MainActivity::class.java, + Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + super.onBackPressed() + } + } + + override fun onDestroy() { + binding = null + super.onDestroy() + } + + /** + * To calculate and display percentage and score + * @param score + */ + @SuppressLint("StringFormatInvalid", "SetTextI18n") + fun setScore(score: Int) { + val scorePercent = score * MULTIPLIER_TO_GET_PERCENTAGE + binding?.resultProgressBar?.progress = scorePercent + binding?.tvResultProgress?.text = "$score / $NUMBER_OF_QUESTIONS" + val message = resources.getString(R.string.congratulatory_message_quiz, "$scorePercent%") + binding?.congratulatoryMessage?.text = message + } + + /** + * To go to Contributions Activity + */ + fun launchContributionActivity() { + startActivityWithFlags( + this, MainActivity::class.java, + Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + } + + override fun onBackPressed() { + startActivityWithFlags( + this, MainActivity::class.java, + Intent.FLAG_ACTIVITY_CLEAR_TOP, Intent.FLAG_ACTIVITY_SINGLE_TOP + ) + super.onBackPressed() + } + + /** + * Function to call intent to an activity + * @param context + * @param cls + * @param flags + */ + companion object { + fun startActivityWithFlags(context: Context, cls: Class, vararg flags: Int) { + val intent = Intent(context, cls) + flags.forEach { flag -> intent.addFlags(flag) } + context.startActivity(intent) + } + } + + /** + * To inflate menu + * @param menu + * @return + */ + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_about, menu) + return true + } + + /** + * If share option selected then take screenshot and launch alert + * @param item + * @return + */ + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.share_app_icon) { + val rootView = window.decorView.findViewById(android.R.id.content) + val screenShot = getScreenShot(rootView) + showAlert(screenShot) + } + return super.onOptionsItemSelected(item) + } + + /** + * To store the screenshot of image in bitmap variable temporarily + * @param view + * @return + */ + fun getScreenShot(view: View): Bitmap { + val screenView = view.rootView + screenView.isDrawingCacheEnabled = true + val bitmap = Bitmap.createBitmap(screenView.drawingCache) + screenView.isDrawingCacheEnabled = false + return bitmap + } + + /** + * Share the screenshot through social media + * @param bitmap + */ + @SuppressLint("SetWorldReadable") + fun shareScreen(bitmap: Bitmap) { + try { + val file = File(this.externalCacheDir, "screen.png") + FileOutputStream(file).use { fOut -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut) + fOut.flush() + } + file.setReadable(true, false) + val intent = Intent(Intent.ACTION_SEND).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)) + type = "image/png" + } + startActivity(Intent.createChooser(intent, getString(R.string.share_image_via))) + } catch (e: Exception) { + e.printStackTrace() + } + } + + /** + * It displays the AlertDialog with Image of screenshot + * @param screenshot + */ + fun showAlert(screenshot: Bitmap) { + val alertadd = AlertDialog.Builder(this) + val factory = LayoutInflater.from(this) + val view = factory.inflate(R.layout.image_alert_layout, null) + val screenShotImage = view.findViewById(R.id.alert_image) + screenShotImage.setImageBitmap(screenshot) + val shareMessage = view.findViewById(R.id.alert_text) + shareMessage.setText(R.string.quiz_result_share_message) + alertadd.setView(view) + alertadd.setPositiveButton(R.string.about_translate_proceed) { dialog, _ -> + shareScreen(screenshot) + } + alertadd.setNegativeButton(android.R.string.cancel) { dialog, _ -> + dialog.cancel() + } + alertadd.show() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java deleted file mode 100644 index 79756871d8..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.java +++ /dev/null @@ -1,64 +0,0 @@ -package fr.free.nrw.commons.quiz; - -import android.app.Activity; -import android.view.View; -import android.widget.CompoundButton; -import android.widget.RadioButton; - -import java.util.ArrayList; -import java.util.List; - -/** - * Used to group to or more radio buttons to ensure - * that at a particular time only one of them is selected - */ -public class RadioGroupHelper { - - public List radioButtons = new ArrayList<>(); - - /** - * Constructor to group radio buttons - * @param radios - */ - public RadioGroupHelper(RadioButton... radios) { - super(); - for (RadioButton rb : radios) { - add(rb); - } - } - - /** - * Constructor to group radio buttons - * @param activity - * @param radiosIDs - */ - public RadioGroupHelper(Activity activity, int... radiosIDs) { - this(activity.findViewById(android.R.id.content),radiosIDs); - } - - /** - * Constructor to group radio buttons - * @param rootView - * @param radiosIDs - */ - public RadioGroupHelper(View rootView, int... radiosIDs) { - super(); - for (int radioButtonID : radiosIDs) { - add(rootView.findViewById(radioButtonID)); - } - } - - private void add(CompoundButton button){ - this.radioButtons.add(button); - button.setOnClickListener(onClickListener); - } - - /** - * listener to ensure only one of the radio button is selected - */ - View.OnClickListener onClickListener = v -> { - for (CompoundButton rb : radioButtons) { - if (rb != v) rb.setChecked(false); - } - }; -} diff --git a/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt new file mode 100644 index 0000000000..8afdf94c5c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/quiz/RadioGroupHelper.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.quiz + +import android.app.Activity +import android.view.View +import android.widget.CompoundButton +import android.widget.RadioButton + +import java.util.ArrayList + +/** + * Used to group to or more radio buttons to ensure + * that at a particular time only one of them is selected + */ +class RadioGroupHelper { + + val radioButtons: MutableList = ArrayList() + + /** + * Constructor to group radio buttons + * @param radios + */ + constructor(vararg radios: RadioButton) { + for (rb in radios) { + add(rb) + } + } + + /** + * Constructor to group radio buttons + * @param activity + * @param radiosIDs + */ + constructor(activity: Activity, vararg radiosIDs: Int) : this( + *radiosIDs.map { id -> activity.findViewById(id) }.toTypedArray() + ) + + /** + * Constructor to group radio buttons + * @param rootView + * @param radiosIDs + */ + constructor(rootView: View, vararg radiosIDs: Int) { + for (radioButtonID in radiosIDs) { + add(rootView.findViewById(radioButtonID)) + } + } + + private fun add(button: CompoundButton) { + radioButtons.add(button) + button.setOnClickListener(onClickListener) + } + + /** + * listener to ensure only one of the radio button is selected + */ + private val onClickListener = View.OnClickListener { v -> + for (rb in radioButtons) { + if (rb != v) rb.isChecked = false + } + } +} From 874773b88131f47829325bb12dc9a3247adf70fa Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Mon, 25 Nov 2024 13:01:54 +0100 Subject: [PATCH 034/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-iw/strings.xml | 10 ++++++++++ app/src/main/res/values-mk/strings.xml | 2 +- app/src/main/res/values-ru/strings.xml | 8 ++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 2ea71f748a..be45099a90 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -149,6 +149,7 @@ חיפוש קטגוריות חפשו פריטים שהמדיה שלך מציגה (הר, טאג\' מהאל, וכו\') שמירה + תפריט גלישה רענון רשימה (לא הועלה עדיין שום דבר) @@ -821,6 +822,15 @@ ממתינות נכשלו לא היה אפשר לטעון את נתוני המקום + מחיקת תיקייה + אישור מחיקה + למחוק את התיקייה %1$s על כל %2$d פריטיה? + מחיקה + ביטול + התיקייה %1$s נמחקה + מחיקת התיקייה %1$s נכשלה + שגיאה בהעברת תוכן התיקייה לאשפה: %1$s + נכשל אחזור נתיב התיקייה למזהה ההקבצה: %1$d אין עדיין תמונה למקום הזה, אפשר פשוט לצלם אחת! למקום הזה כבר יש תמונה. עכשיו מתבצעת בדיקה האם למקום הזה יש תמונה. diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 61d71ea683..6d705c4a78 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -653,7 +653,7 @@ Погл. категориска страница Погл. страница на предметот Јазик на прилогот - Острани толкување и опис + Отстрани толкување и опис Прочитајте повеќе На сите јазици Изберете местоположба diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 54acc52732..9b95e69d1b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -846,6 +846,14 @@ В ожидании Не удалось Не удалось загрузить данные о месте + Удалить папку + Подтвердите удаление + Вы уверены, что хотите удалить папку %1$s содержащую %2$d шт. вложенных элементов? + Удалить + Отмена + Папка %1$s успешно удалена + Не получилось удалить папку %1$s + Ошибка при удалении содержимого папки: %1$s У этого места пока нет фотографий, так что сделайте несколько! У этого места уже есть фотография. Сейчас проверим, есть ли у этого места фотография. From 381f9eca0c73338332546ffb9c1f39797b242cef Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Tue, 26 Nov 2024 17:59:31 +0530 Subject: [PATCH 035/231] Migrated notification module from Java to Kotlin (#5955) * Rename .java to .kt * Migration of notification module from Java to Kotlin --- .../contributions/ContributionsFragment.java | 2 +- .../commons/contributions/MainActivity.java | 2 +- .../notification/NotificationActivity.java | 288 ------------------ .../notification/NotificationActivity.kt | 247 +++++++++++++++ ...catinAdapter.kt => NotificationAdapter.kt} | 2 +- .../notification/NotificationController.java | 33 -- .../notification/NotificationController.kt | 25 ++ .../notification/NotificationHelper.java | 73 ----- .../notification/NotificationHelper.kt | 73 +++++ .../NotificationWorkerFragment.java | 31 -- .../NotificationWorkerFragment.kt | 20 ++ .../notification/models/NotificationType.java | 28 -- .../notification/models/NotificationType.kt | 33 ++ 13 files changed, 401 insertions(+), 456 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt rename app/src/main/java/fr/free/nrw/commons/notification/{NotificatinAdapter.kt => NotificationAdapter.kt} (92%) delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java create mode 100644 app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java index 1699f35f0e..bffafaef15 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsFragment.java @@ -289,7 +289,7 @@ public void onCreateOptionsMenu(@NonNull final Menu menu, }); } notification.setOnClickListener(view -> { - NotificationActivity.startYourself(getContext(), "unread"); + NotificationActivity.Companion.startYourself(getContext(), "unread"); }); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java index 849ef3450b..03027f287a 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/MainActivity.java @@ -414,7 +414,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return true; case R.id.notifications: // Starts notification activity on click to notification icon - NotificationActivity.startYourself(this, "unread"); + NotificationActivity.Companion.startYourself(this, "unread"); return true; default: return super.onOptionsItemSelected(item); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java deleted file mode 100644 index b57df49485..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ /dev/null @@ -1,288 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.Menu; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import com.google.android.material.snackbar.Snackbar; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.databinding.ActivityNotificationBinding; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; -import fr.free.nrw.commons.notification.models.Notification; -import fr.free.nrw.commons.notification.models.NotificationType; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.NetworkUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.Observable; -import io.reactivex.ObservableSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.Disposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.Callable; -import javax.inject.Inject; -import kotlin.Unit; -import timber.log.Timber; - -/** - * Created by root on 18.12.2017. - */ - -public class NotificationActivity extends BaseActivity { - private ActivityNotificationBinding binding; - - @Inject - NotificationController controller; - - @Inject - SessionManager sessionManager; - - private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; - private NotificationWorkerFragment mNotificationWorkerFragment; - private NotificatinAdapter adapter; - private List notificationList; - MenuItem notificationMenuItem; - /** - * Boolean isRead is true if this notification activity is for read section of notification. - */ - private boolean isRead; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - isRead = getIntent().getStringExtra("title").equals("read"); - binding = ActivityNotificationBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - mNotificationWorkerFragment = (NotificationWorkerFragment) getFragmentManager() - .findFragmentByTag(TAG_NOTIFICATION_WORKER_FRAGMENT); - initListView(); - setPageTitle(); - setSupportActionBar(binding.toolbar.toolbar); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - public boolean onSupportNavigateUp() { - onBackPressed(); - return true; - } - - /** - * If this is unread section of the notifications, removeNotification method - * Marks the notification as read, - * Removes the notification from unread, - * Displays the Snackbar. - * - * Otherwise returns (read section). - * - * @param notification - */ - @SuppressLint("CheckResult") - public void removeNotification(Notification notification) { - if (isRead) { - return; - } - Disposable disposable = Observable.defer((Callable>) - () -> controller.markAsRead(notification)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(result -> { - if (result) { - notificationList.remove(notification); - setItems(notificationList); - adapter.notifyDataSetChanged(); - ViewUtil.showLongSnackbar(binding.container,getString(R.string.notification_mark_read)); - if (notificationList.size() == 0) { - setEmptyView(); - binding.container.setVisibility(View.GONE); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - } - } else { - adapter.notifyDataSetChanged(); - setItems(notificationList); - ViewUtil.showLongToast(this,getString(R.string.some_error)); - } - }, throwable -> { - if (throwable instanceof InvalidLoginTokenException) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - this, - getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - this, logoutListener); - } else { - Timber.e(throwable, "Error occurred while loading notifications"); - throwable.printStackTrace(); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - } - binding.progressBar.setVisibility(View.GONE); - }); - getCompositeDisposable().add(disposable); - } - - - - private void initListView() { - binding.listView.setLayoutManager(new LinearLayoutManager(this)); - DividerItemDecoration itemDecor = new DividerItemDecoration(binding.listView.getContext(), DividerItemDecoration.VERTICAL); - binding.listView.addItemDecoration(itemDecor); - if (isRead) { - refresh(true); - } else { - refresh(false); - } - adapter = new NotificatinAdapter(item -> { - Timber.d("Notification clicked %s", item.getLink()); - if (item.getNotificationType() == NotificationType.EMAIL){ - ViewUtil.showLongSnackbar(binding.container,getString(R.string.check_your_email_inbox)); - } else { - handleUrl(item.getLink()); - } - removeNotification(item); - return Unit.INSTANCE; - }); - binding.listView.setAdapter(adapter); - } - - private void refresh(boolean archived) { - if (!NetworkUtils.isInternetConnectionEstablished(this)) { - binding.progressBar.setVisibility(View.GONE); - Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.retry, view -> refresh(archived)).show(); - } else { - addNotifications(archived); - } - binding.progressBar.setVisibility(View.VISIBLE); - binding.noNotificationBackground.setVisibility(View.GONE); - binding.container.setVisibility(View.VISIBLE); - } - - @SuppressLint("CheckResult") - private void addNotifications(boolean archived) { - Timber.d("Add notifications"); - if (mNotificationWorkerFragment == null) { - binding.progressBar.setVisibility(View.VISIBLE); - getCompositeDisposable().add(controller.getNotifications(archived) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(notificationList -> { - Collections.reverse(notificationList); - Timber.d("Number of notifications is %d", notificationList.size()); - this.notificationList = notificationList; - if (notificationList.size()==0){ - setEmptyView(); - binding.container.setVisibility(View.GONE); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - } else { - setItems(notificationList); - } - binding.progressBar.setVisibility(View.GONE); - }, throwable -> { - Timber.e(throwable, "Error occurred while loading notifications "); - ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications); - binding.progressBar.setVisibility(View.GONE); - })); - } else { - notificationList = mNotificationWorkerFragment.getNotificationList(); - setItems(notificationList); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getMenuInflater(); - inflater.inflate(R.menu.menu_notifications, menu); - notificationMenuItem = menu.findItem(R.id.archived); - setMenuItemTitle(); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle item selection - switch (item.getItemId()) { - case R.id.archived: - if (item.getTitle().equals(getString(R.string.menu_option_read))) { - NotificationActivity.startYourself(NotificationActivity.this, "read"); - }else if (item.getTitle().equals(getString(R.string.menu_option_unread))) { - onBackPressed(); - } - return true; - default: - return super.onOptionsItemSelected(item); - } - } - - private void handleUrl(String url) { - if (url == null || url.equals("")) { - return; - } - Utils.handleWebUrl(this, Uri.parse(url)); - } - - private void setItems(List notificationList) { - if (notificationList == null || notificationList.isEmpty()) { - ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications); - /*progressBar.setVisibility(View.GONE); - recyclerView.setVisibility(View.GONE);*/ - binding.container.setVisibility(View.GONE); - setEmptyView(); - binding.noNotificationBackground.setVisibility(View.VISIBLE); - return; - } - binding.container.setVisibility(View.VISIBLE); - binding.noNotificationBackground.setVisibility(View.GONE); - adapter.setItems(notificationList); - } - - public static void startYourself(Context context, String title) { - Intent intent = new Intent(context, NotificationActivity.class); - intent.putExtra("title", title); - - context.startActivity(intent); - } - - private void setPageTitle() { - if (getSupportActionBar() != null) { - if (isRead) { - getSupportActionBar().setTitle(R.string.read_notifications); - } else { - getSupportActionBar().setTitle(R.string.notifications); - } - } - } - - private void setEmptyView() { - if (isRead) { - binding.noNotificationText.setText(R.string.no_read_notification); - }else { - binding.noNotificationText.setText(R.string.no_notification); - } - } - - private void setMenuItemTitle() { - if (isRead) { - notificationMenuItem.setTitle(R.string.menu_option_unread); - - }else { - notificationMenuItem.setTitle(R.string.menu_option_read); - - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt new file mode 100644 index 0000000000..1d87a8f82c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -0,0 +1,247 @@ +package fr.free.nrw.commons.notification + +import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.View +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException +import fr.free.nrw.commons.databinding.ActivityNotificationBinding +import fr.free.nrw.commons.notification.models.Notification +import fr.free.nrw.commons.notification.models.NotificationType +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.NetworkUtils +import fr.free.nrw.commons.utils.ViewUtil +import io.reactivex.Observable +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import javax.inject.Inject + +/** + * Created by root on 18.12.2017. + */ +class NotificationActivity : BaseActivity() { + + private lateinit var binding: ActivityNotificationBinding + + @Inject + lateinit var controller: NotificationController + + @Inject + lateinit var sessionManager: SessionManager + + private val tagNotificationWorkerFragment = "NotificationWorkerFragment" + private var mNotificationWorkerFragment: NotificationWorkerFragment? = null + private lateinit var adapter: NotificationAdapter + private var notificationList: MutableList = mutableListOf() + private var notificationMenuItem: MenuItem? = null + + /** + * Boolean isRead is true if this notification activity is for read section of notification. + */ + private var isRead: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + isRead = intent.getStringExtra("title") == "read" + binding = ActivityNotificationBinding.inflate(layoutInflater) + setContentView(binding.root) + mNotificationWorkerFragment = supportFragmentManager.findFragmentByTag( + tagNotificationWorkerFragment + ) as? NotificationWorkerFragment + initListView() + setPageTitle() + setSupportActionBar(binding.toolbar.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } + + override fun onSupportNavigateUp(): Boolean { + onBackPressed() + return true + } + + @SuppressLint("CheckResult", "NotifyDataSetChanged") + fun removeNotification(notification: Notification) { + if (isRead) return + + val disposable = Observable.defer { controller.markAsRead(notification) } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ result -> + if (result) { + notificationList.remove(notification) + setItems(notificationList) + adapter.notifyDataSetChanged() + ViewUtil.showLongSnackbar(binding.container, getString(R.string.notification_mark_read)) + if (notificationList.isEmpty()) { + setEmptyView() + binding.container.visibility = View.GONE + binding.noNotificationBackground.visibility = View.VISIBLE + } + } else { + adapter.notifyDataSetChanged() + setItems(notificationList) + ViewUtil.showLongToast(this, getString(R.string.some_error)) + } + }, { throwable -> + if (throwable is InvalidLoginTokenException) { + val username = sessionManager.getUserName() + val logoutListener = CommonsApplication.BaseLogoutListener( + this, + getString(R.string.invalid_login_message), + username + ) + CommonsApplication.instance.clearApplicationData(this, logoutListener) + } else { + Timber.e(throwable, "Error occurred while loading notifications") + ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications) + } + binding.progressBar.visibility = View.GONE + }) + compositeDisposable.add(disposable) + } + + private fun initListView() { + binding.listView.layoutManager = LinearLayoutManager(this) + val itemDecor = DividerItemDecoration(binding.listView.context, DividerItemDecoration.VERTICAL) + binding.listView.addItemDecoration(itemDecor) + refresh(isRead) + adapter = NotificationAdapter { item -> + Timber.d("Notification clicked %s", item.link) + if (item.notificationType == NotificationType.EMAIL) { + ViewUtil.showLongSnackbar(binding.container, getString(R.string.check_your_email_inbox)) + } else { + handleUrl(item.link) + } + removeNotification(item) + } + binding.listView.adapter = adapter + } + + private fun refresh(archived: Boolean) { + if (!NetworkUtils.isInternetConnectionEstablished(this)) { + binding.progressBar.visibility = View.GONE + Snackbar.make(binding.container, R.string.no_internet, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.retry) { refresh(archived) } + .show() + } else { + addNotifications(archived) + } + binding.progressBar.visibility = View.VISIBLE + binding.noNotificationBackground.visibility = View.GONE + binding.container.visibility = View.VISIBLE + } + + @SuppressLint("CheckResult") + private fun addNotifications(archived: Boolean) { + Timber.d("Add notifications") + if (mNotificationWorkerFragment == null) { + binding.progressBar.visibility = View.VISIBLE + compositeDisposable.add(controller.getNotifications(archived) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ notificationList -> + notificationList.reversed() + Timber.d("Number of notifications is %d", notificationList.size) + this.notificationList = notificationList.toMutableList() + if (notificationList.isEmpty()) { + setEmptyView() + binding.container.visibility = View.GONE + binding.noNotificationBackground.visibility = View.VISIBLE + } else { + setItems(notificationList) + } + binding.progressBar.visibility = View.GONE + }, { throwable -> + Timber.e(throwable, "Error occurred while loading notifications") + ViewUtil.showShortSnackbar(binding.container, R.string.error_notifications) + binding.progressBar.visibility = View.GONE + })) + } else { + notificationList = mNotificationWorkerFragment?.notificationList?.toMutableList() ?: mutableListOf() + setItems(notificationList) + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.menu_notifications, menu) + notificationMenuItem = menu.findItem(R.id.archived) + setMenuItemTitle() + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.archived -> { + if (item.title == getString(R.string.menu_option_read)) { + startYourself(this, "read") + } else if (item.title == getString(R.string.menu_option_unread)) { + onBackPressed() + } + true + } + else -> super.onOptionsItemSelected(item) + } + } + + private fun handleUrl(url: String?) { + if (url.isNullOrEmpty()) return + Utils.handleWebUrl(this, Uri.parse(url)) + } + + private fun setItems(notificationList: List?) { + if (notificationList.isNullOrEmpty()) { + ViewUtil.showShortSnackbar(binding.container, R.string.no_notifications) + binding.container.visibility = View.GONE + setEmptyView() + binding.noNotificationBackground.visibility = View.VISIBLE + return + } + binding.container.visibility = View.VISIBLE + binding.noNotificationBackground.visibility = View.GONE + adapter.items = notificationList + } + + private fun setPageTitle() { + supportActionBar?.title = if (isRead) { + getString(R.string.read_notifications) + } else { + getString(R.string.notifications) + } + } + + private fun setEmptyView() { + binding.noNotificationText.text = if (isRead) { + getString(R.string.no_read_notification) + } else { + getString(R.string.no_notification) + } + } + + private fun setMenuItemTitle() { + notificationMenuItem?.title = if (isRead) { + getString(R.string.menu_option_unread) + } else { + getString(R.string.menu_option_read) + } + } + + companion object { + fun startYourself(context: Context, title: String) { + val intent = Intent(context, NotificationActivity::class.java) + intent.putExtra("title", title) + context.startActivity(intent) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt similarity index 92% rename from app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt rename to app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt index 41d7d48830..637443ecf9 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificatinAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationAdapter.kt @@ -3,7 +3,7 @@ package fr.free.nrw.commons.notification import fr.free.nrw.commons.notification.models.Notification import fr.free.nrw.commons.upload.categories.BaseDelegateAdapter -internal class NotificatinAdapter( +internal class NotificationAdapter( onNotificationClicked: (Notification) -> Unit, ) : BaseDelegateAdapter( notificationDelegate(onNotificationClicked), diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java deleted file mode 100644 index de1f372d2d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.java +++ /dev/null @@ -1,33 +0,0 @@ -package fr.free.nrw.commons.notification; - -import fr.free.nrw.commons.notification.models.Notification; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import io.reactivex.Observable; -import io.reactivex.Single; - -/** - * Created by root on 19.12.2017. - */ -@Singleton -public class NotificationController { - - private NotificationClient notificationClient; - - - @Inject - public NotificationController(NotificationClient notificationClient) { - this.notificationClient = notificationClient; - } - - public Single> getNotifications(boolean archived) { - return notificationClient.getNotifications(archived); - } - - Observable markAsRead(Notification notification) { - return notificationClient.markNotificationAsRead(notification.getNotificationId()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt new file mode 100644 index 0000000000..870d658cb6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationController.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.notification + +import fr.free.nrw.commons.notification.models.Notification +import javax.inject.Inject +import javax.inject.Singleton + +import io.reactivex.Observable +import io.reactivex.Single + +/** + * Created by root on 19.12.2017. + */ +@Singleton +class NotificationController @Inject constructor( + private val notificationClient: NotificationClient +) { + + fun getNotifications(archived: Boolean): Single> { + return notificationClient.getNotifications(archived) + } + + fun markAsRead(notification: Notification): Observable { + return notificationClient.markNotificationAsRead(notification.notificationId) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java deleted file mode 100644 index b63d3a4c10..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.java +++ /dev/null @@ -1,73 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import androidx.core.app.NotificationCompat; -import javax.inject.Inject; -import javax.inject.Singleton; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import static androidx.core.app.NotificationCompat.DEFAULT_ALL; -import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; - -/** - * Helper class that can be used to build a generic notification - * Going forward all notifications should be built using this helper class - */ -@Singleton -public class NotificationHelper { - - public static final int NOTIFICATION_DELETE = 1; - public static final int NOTIFICATION_EDIT_CATEGORY = 2; - public static final int NOTIFICATION_EDIT_COORDINATES = 3; - public static final int NOTIFICATION_EDIT_DESCRIPTION = 4; - public static final int NOTIFICATION_EDIT_DEPICTIONS = 5; - - private final NotificationManager notificationManager; - private final NotificationCompat.Builder notificationBuilder; - - @Inject - public NotificationHelper(final Context context) { - notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationBuilder = new NotificationCompat - .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) - .setOnlyAlertOnce(true); - } - - /** - * Public interface to build and show a notification in the notification bar - * @param context passed context - * @param notificationTitle title of the notification - * @param notificationMessage message to be displayed in the notification - * @param notificationId the notificationID - * @param intent the intent to be fired when the notification is clicked - */ - public void showNotification( - final Context context, - final String notificationTitle, - final String notificationMessage, - final int notificationId, - final Intent intent - ) { - notificationBuilder.setDefaults(DEFAULT_ALL) - .setContentTitle(notificationTitle) - .setStyle(new NotificationCompat.BigTextStyle() - .bigText(notificationMessage)) - .setSmallIcon(R.drawable.ic_launcher) - .setProgress(0, 0, false) - .setOngoing(false) - .setPriority(PRIORITY_HIGH); - - int flags = PendingIntent.FLAG_UPDATE_CURRENT; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - flags |= PendingIntent.FLAG_IMMUTABLE; // This flag was introduced in API 23 - } - - final PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, flags); - notificationBuilder.setContentIntent(pendingIntent); - notificationManager.notify(notificationId, notificationBuilder.build()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt new file mode 100644 index 0000000000..101a8fccc6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationHelper.kt @@ -0,0 +1,73 @@ +package fr.free.nrw.commons.notification + +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import javax.inject.Inject +import javax.inject.Singleton +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import androidx.core.app.NotificationCompat.DEFAULT_ALL +import androidx.core.app.NotificationCompat.PRIORITY_HIGH + +/** + * Helper class that can be used to build a generic notification + * Going forward all notifications should be built using this helper class + */ +@Singleton +class NotificationHelper @Inject constructor( + context: Context +) { + + companion object { + const val NOTIFICATION_DELETE = 1 + const val NOTIFICATION_EDIT_CATEGORY = 2 + const val NOTIFICATION_EDIT_COORDINATES = 3 + const val NOTIFICATION_EDIT_DESCRIPTION = 4 + const val NOTIFICATION_EDIT_DEPICTIONS = 5 + } + + private val notificationManager: NotificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private val notificationBuilder: NotificationCompat.Builder = NotificationCompat + .Builder(context, CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL) + .setOnlyAlertOnce(true) + + /** + * Public interface to build and show a notification in the notification bar + * @param context passed context + * @param notificationTitle title of the notification + * @param notificationMessage message to be displayed in the notification + * @param notificationId the notificationID + * @param intent the intent to be fired when the notification is clicked + */ + fun showNotification( + context: Context, + notificationTitle: String, + notificationMessage: String, + notificationId: Int, + intent: Intent + ) { + notificationBuilder.setDefaults(NotificationCompat.DEFAULT_ALL) + .setContentTitle(notificationTitle) + .setStyle(NotificationCompat.BigTextStyle().bigText(notificationMessage)) + .setSmallIcon(R.drawable.ic_launcher) + .setProgress(0, 0, false) + .setOngoing(false) + .setPriority(NotificationCompat.PRIORITY_HIGH) + + val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + + val pendingIntent = PendingIntent.getActivity(context, 1, intent, flags) + notificationBuilder.setContentIntent(pendingIntent) + notificationManager.notify(notificationId, notificationBuilder.build()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java deleted file mode 100644 index ffee5eac28..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.notification; - -import android.app.Fragment; -import android.os.Bundle; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.notification.models.Notification; -import java.util.List; - -/** - * Created by knightshade on 25/2/18. - */ - -public class NotificationWorkerFragment extends Fragment { - private List notificationList; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setRetainInstance(true); - } - - public void setNotificationList(List notificationList){ - this.notificationList = notificationList; - } - - public List getNotificationList(){ - return notificationList; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt new file mode 100644 index 0000000000..928651b9a0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationWorkerFragment.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.notification + +import android.app.Fragment +import android.os.Bundle + +import fr.free.nrw.commons.notification.models.Notification + + +/** + * Created by knightshade on 25/2/18. + */ +class NotificationWorkerFragment : Fragment() { + + var notificationList: List? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + retainInstance = true + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java deleted file mode 100644 index fb9ae7e994..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.java +++ /dev/null @@ -1,28 +0,0 @@ -package fr.free.nrw.commons.notification.models; - -public enum NotificationType { - THANK_YOU_EDIT("thank-you-edit"), - EDIT_USER_TALK("edit-user-talk"), - MENTION("mention"), - EMAIL("email"), - WELCOME("welcome"), - UNKNOWN("unknown"); - private String type; - - NotificationType(String type) { - this.type = type; - } - - public String getType() { - return type; - } - - public static NotificationType handledValueOf(String name) { - for (NotificationType e : values()) { - if (e.getType().equals(name)) { - return e; - } - } - return UNKNOWN; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt new file mode 100644 index 0000000000..9034b3c59e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/models/NotificationType.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.notification.models + +enum class NotificationType(private val type: String) { + THANK_YOU_EDIT("thank-you-edit"), + + EDIT_USER_TALK("edit-user-talk"), + + MENTION("mention"), + + EMAIL("email"), + + WELCOME("welcome"), + + UNKNOWN("unknown"); + + // Getter for the type property + fun getType(): String { + return type + } + + companion object { + // Returns the corresponding NotificationType for a given name or UNKNOWN + // if no match is found + fun handledValueOf(name: String): NotificationType { + for (e in values()) { + if (e.type == name) { + return e + } + } + return UNKNOWN + } + } +} From 238023056fbc97ec95f98d1870b3e0e57bf53e5b Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Wed, 27 Nov 2024 19:28:46 +0530 Subject: [PATCH 036/231] Migrated navtab module from Java to Kotlin (#5965) * Rename .java to .kt * Migrated navtab module from Java to Kotlin * Migrated navtab module from Java to Kotlin --- .../navtab/MoreBottomSheetFragment.java | 238 ----------------- .../commons/navtab/MoreBottomSheetFragment.kt | 242 ++++++++++++++++++ .../MoreBottomSheetLoggedOutFragment.java | 142 ---------- .../MoreBottomSheetLoggedOutFragment.kt | 151 +++++++++++ .../fr/free/nrw/commons/navtab/NavTab.java | 95 ------- .../java/fr/free/nrw/commons/navtab/NavTab.kt | 79 ++++++ .../navtab/NavTabFragmentPagerAdapter.java | 38 --- .../navtab/NavTabFragmentPagerAdapter.kt | 36 +++ .../free/nrw/commons/navtab/NavTabLayout.java | 41 --- .../free/nrw/commons/navtab/NavTabLayout.kt | 47 ++++ .../nrw/commons/navtab/NavTabLoggedOut.java | 79 ------ .../nrw/commons/navtab/NavTabLoggedOut.kt | 65 +++++ 12 files changed, 620 insertions(+), 633 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java create mode 100644 app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java deleted file mode 100644 index 9ea59488ed..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.java +++ /dev/null @@ -1,238 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.actions.PageEditClient; -import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.feedback.FeedbackContentCreator; -import fr.free.nrw.commons.feedback.model.Feedback; -import fr.free.nrw.commons.feedback.FeedbackDialog; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.review.ReviewActivity; -import fr.free.nrw.commons.settings.SettingsActivity; -import io.reactivex.Single; -import io.reactivex.SingleSource; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.concurrent.Callable; -import javax.inject.Inject; -import javax.inject.Named; - -public class MoreBottomSheetFragment extends BottomSheetDialogFragment { - - @Inject - CommonsLogSender commonsLogSender; - - private TextView moreProfile; - - @Inject @Named("default_preferences") - JsonKvStore store; - - @Inject - @Named("commons-page-edit") - PageEditClient pageEditClient; - - private static final String GITHUB_ISSUES_URL = "https://github.com/commons-app/apps-android-commons/issues"; - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - super.onCreateView(inflater, container, savedInstanceState); - final @NonNull FragmentMoreBottomSheetBinding binding = - FragmentMoreBottomSheetBinding.inflate(inflater, container, false); - moreProfile = binding.moreProfile; - - if(store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)){ - binding.morePeerReview.setVisibility(View.GONE); - } - - binding.moreLogout.setOnClickListener(v -> onLogoutClicked()); - binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked()); - binding.moreAbout.setOnClickListener(v -> onAboutClicked()); - binding.moreTutorial.setOnClickListener(v -> onTutorialClicked()); - binding.moreSettings.setOnClickListener(v -> onSettingsClicked()); - binding.moreProfile.setOnClickListener(v -> onProfileClicked()); - binding.morePeerReview.setOnClickListener(v -> onPeerReviewClicked()); - binding.moreFeedbackGithub.setOnClickListener(v -> onFeedbackGithubClicked()); - - setUserName(); - return binding.getRoot(); - } - - private void onFeedbackGithubClicked() { - final Intent intent; - intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse(GITHUB_ISSUES_URL)); - startActivity(intent); - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - ApplicationlessInjection - .getInstance(requireActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - } - - /** - * Set the username and user achievements level (if available) in navigationHeader. - */ - private void setUserName() { - BasicKvStore store = new BasicKvStore(this.getContext(), getUserName()); - String level = store.getString("userAchievementsLevel","0"); - if (level.equals("0")) { - moreProfile.setText(getUserName() + " (" + getString(R.string.see_your_achievements) + ")"); - } - else { - moreProfile.setText(getUserName() + " (" + getString(R.string.level) + " " + level + ")"); - } - } - - private String getUserName(){ - final AccountManager accountManager = AccountManager.get(getActivity()); - final Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - return allAccounts[0].name; - } - return ""; - } - - - protected void onLogoutClicked() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.logout_verification) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, which) -> { - final CommonsApplication app = (CommonsApplication) - requireContext().getApplicationContext(); - app.clearApplicationData(requireContext(), new ActivityLogoutListener(requireActivity(), getContext())); - }) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) - .show(); - } - - protected void onFeedbackClicked() { - showFeedbackDialog(); - } - - /** - * Creates and shows a dialog asking feedback from users - */ - private void showFeedbackDialog() { - new FeedbackDialog(getContext(), this::uploadFeedback).show(); - } - - /** - * uploads feedback data on the server - */ - void uploadFeedback(final Feedback feedback) { - final FeedbackContentCreator feedbackContentCreator = new FeedbackContentCreator(getContext(), feedback); - - final Single single = - pageEditClient.createNewSection( - "Commons:Mobile_app/Feedback", - feedbackContentCreator.getSectionTitle(), - feedbackContentCreator.getSectionText(), - "New feedback on version " + feedback.getVersion() + " of the app" - ) - .flatMapSingle(Single::just) - .firstOrError(); - - Single.defer((Callable>) () -> single) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(aBoolean -> { - if (aBoolean) { - Toast.makeText(getContext(), getString(R.string.thanks_feedback), Toast.LENGTH_SHORT) - .show(); - } else { - Toast.makeText(getContext(), getString(R.string.error_feedback), - Toast.LENGTH_SHORT).show(); - } - }); - } - - /** - * This method shows the alert dialog when a user wants to send feedback about the app. - */ - private void showAlertDialog() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.feedback_sharing_data_alert) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, which) -> sendFeedback()) - .show(); - } - - /** - * This method collects the feedback message and starts the activity with implicit intent - * to available email client. - */ - private void sendFeedback() { - final String technicalInfo = commonsLogSender.getExtraInfo(); - - final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.setData(Uri.parse("mailto:")); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.FEEDBACK_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - CommonsApplication.FEEDBACK_EMAIL_SUBJECT); - feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format( - "\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo)); - try { - startActivity(feedbackIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - } - - protected void onAboutClicked() { - final Intent intent = new Intent(getActivity(), AboutActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - protected void onTutorialClicked() { - WelcomeActivity.startYourself(getActivity()); - } - - protected void onSettingsClicked() { - final Intent intent = new Intent(getActivity(), SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - protected void onProfileClicked() { - ProfileActivity.startYourself(getActivity(), getUserName(), false); - } - - protected void onPeerReviewClicked() { - ReviewActivity.Companion.startYourself(getActivity(), getString(R.string.title_activity_review)); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt new file mode 100644 index 0000000000..857e18ec31 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -0,0 +1,242 @@ +package fr.free.nrw.commons.navtab + +import android.accounts.AccountManager +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.CommonsApplication.ActivityLogoutListener +import fr.free.nrw.commons.R +import fr.free.nrw.commons.WelcomeActivity +import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.feedback.FeedbackContentCreator +import fr.free.nrw.commons.feedback.FeedbackDialog +import fr.free.nrw.commons.feedback.model.Feedback +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.review.ReviewActivity +import fr.free.nrw.commons.settings.SettingsActivity +import io.reactivex.Single +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import javax.inject.Inject +import javax.inject.Named + + +class MoreBottomSheetFragment : BottomSheetDialogFragment() { + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + @field: Named("default_preferences") + lateinit var store: JsonKvStore + + @Inject + @field: Named("commons-page-edit") + lateinit var pageEditClient: PageEditClient + + companion object { + private const val GITHUB_ISSUES_URL = + "https://github.com/commons-app/apps-android-commons/issues" + } + + private var binding: FragmentMoreBottomSheetBinding? = null + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMoreBottomSheetBinding.inflate(inflater, container, false) + + if (store.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED)) { + binding?.morePeerReview?.visibility = View.GONE + } + + binding?.apply { + moreLogout.setOnClickListener { onLogoutClicked() } + moreFeedback.setOnClickListener { onFeedbackClicked() } + moreAbout.setOnClickListener { onAboutClicked() } + moreTutorial.setOnClickListener { onTutorialClicked() } + moreSettings.setOnClickListener { onSettingsClicked() } + moreProfile.setOnClickListener { onProfileClicked() } + morePeerReview.setOnClickListener { onPeerReviewClicked() } + moreFeedbackGithub.setOnClickListener { onFeedbackGithubClicked() } + } + + setUserName() + return binding?.root + } + + private fun onFeedbackGithubClicked() { + val intent = Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(GITHUB_ISSUES_URL) + } + startActivity(intent) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + /** + * Set the username and user achievements level (if available) in navigationHeader. + */ + private fun setUserName() { + val store = BasicKvStore(requireContext(), getUserName()) + val level = store.getString("userAchievementsLevel", "0") + binding?.moreProfile?.text = if (level == "0") { + "${getUserName()} (${getString(R.string.see_your_achievements)})" + } else { + "${getUserName()} (${getString(R.string.level)} $level)" + } + } + + private fun getUserName(): String { + val accountManager = AccountManager.get(requireActivity()) + val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE) + return if (allAccounts.isNotEmpty()) { + allAccounts[0].name + } else { + "" + } + } + + fun onLogoutClicked() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.logout_verification) + .setCancelable(false) + .setPositiveButton(R.string.yes) { _, _ -> + val app = requireContext().applicationContext as CommonsApplication + app.clearApplicationData(requireContext(), ActivityLogoutListener(requireActivity(), requireContext())) + } + .setNegativeButton(R.string.no) { dialog, _ -> dialog.cancel() } + .show() + } + + fun onFeedbackClicked() { + showFeedbackDialog() + } + + /** + * Creates and shows a dialog asking feedback from users + */ + private fun showFeedbackDialog() { + FeedbackDialog(requireContext()) { uploadFeedback(it) }.show() + } + + /** + * Uploads feedback data on the server + */ + @SuppressLint("CheckResult") + fun uploadFeedback(feedback: Feedback) { + val feedbackContentCreator = FeedbackContentCreator(requireContext(), feedback) + + val single = pageEditClient.createNewSection( + "Commons:Mobile_app/Feedback", + feedbackContentCreator.sectionTitle, + feedbackContentCreator.sectionText, + "New feedback on version ${feedback.version} of the app" + ) + .flatMapSingle { Single.just(it) } + .firstOrError() + + Single.defer { single } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { success -> + val messageResId = if (success) { + R.string.thanks_feedback + } else { + R.string.error_feedback + } + Toast.makeText(requireContext(), getString(messageResId), Toast.LENGTH_SHORT).show() + } + } + + /** + * This method shows the alert dialog when a user wants to send feedback about the app. + */ + private fun showAlertDialog() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.feedback_sharing_data_alert) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> sendFeedback() } + .show() + } + + /** + * This method collects the feedback message and starts the activity with implicit intent + * to the available email client. + */ + @SuppressLint("IntentReset") + private fun sendFeedback() { + val technicalInfo = commonsLogSender.getExtraInfo() + + val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply { + type = "message/rfc822" + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT) + putExtra(Intent.EXTRA_TEXT, "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo") + } + + try { + startActivity(feedbackIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() + } + } + + fun onAboutClicked() { + val intent = Intent(activity, AboutActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onTutorialClicked() { + WelcomeActivity.startYourself(requireActivity()) + } + + fun onSettingsClicked() { + val intent = Intent(activity, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onProfileClicked() { + ProfileActivity.startYourself(requireActivity(), getUserName(), false) + } + + fun onPeerReviewClicked() { + ReviewActivity.startYourself(requireActivity(), getString(R.string.title_activity_review)) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java deleted file mode 100644 index 3537d7f7bb..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.java +++ /dev/null @@ -1,142 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AlertDialog; -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.logging.CommonsLogSender; -import fr.free.nrw.commons.settings.SettingsActivity; -import javax.inject.Inject; -import javax.inject.Named; -import timber.log.Timber; - -public class MoreBottomSheetLoggedOutFragment extends BottomSheetDialogFragment { - - private FragmentMoreBottomSheetLoggedOutBinding binding; - @Inject - CommonsLogSender commonsLogSender; - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - - @Nullable - @Override - public View onCreateView(@NonNull final LayoutInflater inflater, - @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { - binding = FragmentMoreBottomSheetLoggedOutBinding.inflate(inflater, container, false); - return binding.getRoot(); - } - - @Override - public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { - binding.moreLogin.setOnClickListener(v -> onLogoutClicked()); - binding.moreFeedback.setOnClickListener(v -> onFeedbackClicked()); - binding.moreAbout.setOnClickListener(v -> onAboutClicked()); - binding.moreSettings.setOnClickListener(v -> onSettingsClicked()); - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; - } - - @Override - public void onAttach(@NonNull final Context context) { - super.onAttach(context); - ApplicationlessInjection - .getInstance(requireActivity().getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - } - - public void onLogoutClicked() { - applicationKvStore.putBoolean("login_skipped", false); - final Intent intent = new Intent(getContext(), LoginActivity.class); - requireActivity().finish(); //Kill the activity from which you will go to next activity - startActivity(intent); - } - - public void onFeedbackClicked() { - showAlertDialog(); - } - - /** - * This method shows the alert dialog when a user wants to send feedback about the app. - */ - private void showAlertDialog() { - new AlertDialog.Builder(requireActivity()) - .setMessage(R.string.feedback_sharing_data_alert) - .setCancelable(false) - .setPositiveButton(R.string.ok, (dialog, which) -> { - sendFeedback(); - }) - .show(); - } - - /** - * This method collects the feedback message and starts and activity with implicit intent to - * available email client. - */ - private void sendFeedback() { - final String technicalInfo = commonsLogSender.getExtraInfo(); - - final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO); - feedbackIntent.setType("message/rfc822"); - feedbackIntent.setData(Uri.parse("mailto:")); - feedbackIntent.putExtra(Intent.EXTRA_EMAIL, - new String[]{CommonsApplication.FEEDBACK_EMAIL}); - feedbackIntent.putExtra(Intent.EXTRA_SUBJECT, - CommonsApplication.FEEDBACK_EMAIL_SUBJECT); - feedbackIntent.putExtra(Intent.EXTRA_TEXT, String.format( - "\n\n%s\n%s", CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER, technicalInfo)); - try { - startActivity(feedbackIntent); - } catch (final ActivityNotFoundException e) { - Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show(); - } - } - - public void onAboutClicked() { - final Intent intent = new Intent(getActivity(), AboutActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - public void onSettingsClicked() { - final Intent intent = new Intent(getActivity(), SettingsActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP); - requireActivity().startActivity(intent); - } - - private class BaseLogoutListener implements CommonsApplication.LogoutListener { - - @Override - public void onLogoutComplete() { - Timber.d("Logout complete callback received."); - final Intent nearbyIntent = new Intent( - getContext(), LoginActivity.class); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - nearbyIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(nearbyIntent); - requireActivity().finish(); - } - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt new file mode 100644 index 0000000000..96baf9e5ea --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetLoggedOutFragment.kt @@ -0,0 +1,151 @@ +package fr.free.nrw.commons.navtab + +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AlertDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.databinding.FragmentMoreBottomSheetLoggedOutBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.logging.CommonsLogSender +import fr.free.nrw.commons.settings.SettingsActivity +import javax.inject.Inject +import javax.inject.Named +import timber.log.Timber + + +class MoreBottomSheetLoggedOutFragment : BottomSheetDialogFragment() { + + private var binding: FragmentMoreBottomSheetLoggedOutBinding? = null + + @Inject + lateinit var commonsLogSender: CommonsLogSender + + @Inject + @field: Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + binding = FragmentMoreBottomSheetLoggedOutBinding.inflate( + inflater, + container, + false + ) + return binding?.root + } + + override fun onViewCreated( + view: View, + savedInstanceState: Bundle? + ) { + binding?.apply { + moreLogin.setOnClickListener { onLogoutClicked() } + moreFeedback.setOnClickListener { onFeedbackClicked() } + moreAbout.setOnClickListener { onAboutClicked() } + moreSettings.setOnClickListener { onSettingsClicked() } + } + } + + override fun onDestroyView() { + super.onDestroyView() + binding = null + } + + override fun onAttach(context: Context) { + super.onAttach(context) + ApplicationlessInjection + .getInstance(requireActivity().applicationContext) + .commonsApplicationComponent + .inject(this) + } + + fun onLogoutClicked() { + applicationKvStore.putBoolean("login_skipped", false) + val intent = Intent(context, LoginActivity::class.java) + requireActivity().finish() // Kill the activity from which you will go to next activity + startActivity(intent) + } + + fun onFeedbackClicked() { + showAlertDialog() + } + + /** + * This method shows the alert dialog when a user wants to send feedback about the app. + */ + private fun showAlertDialog() { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.feedback_sharing_data_alert) + .setCancelable(false) + .setPositiveButton(R.string.ok) { _, _ -> sendFeedback() } + .show() + } + + /** + * This method collects the feedback message and starts an activity with an implicit intent to + * the available email client. + */ + @SuppressLint("IntentReset") + private fun sendFeedback() { + val technicalInfo = commonsLogSender.getExtraInfo() + + val feedbackIntent = Intent(Intent.ACTION_SENDTO).apply { + type = "message/rfc822" + data = Uri.parse("mailto:") + putExtra(Intent.EXTRA_EMAIL, arrayOf(CommonsApplication.FEEDBACK_EMAIL)) + putExtra(Intent.EXTRA_SUBJECT, CommonsApplication.FEEDBACK_EMAIL_SUBJECT) + putExtra( + Intent.EXTRA_TEXT, + "\n\n${CommonsApplication.FEEDBACK_EMAIL_TEMPLATE_HEADER}\n$technicalInfo" + ) + } + + try { + startActivity(feedbackIntent) + } catch (e: ActivityNotFoundException) { + Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show() + } + } + + fun onAboutClicked() { + val intent = Intent(activity, AboutActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + fun onSettingsClicked() { + val intent = Intent(activity, SettingsActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + requireActivity().startActivity(intent) + } + + private inner class BaseLogoutListener : CommonsApplication.LogoutListener { + + override fun onLogoutComplete() { + Timber.d("Logout complete callback received.") + val nearbyIntent = Intent(context, LoginActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(nearbyIntent) + requireActivity().finish() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java deleted file mode 100644 index 0a3123c1cd..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.java +++ /dev/null @@ -1,95 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; - -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; - -import fr.free.nrw.commons.R; - - -public enum NavTab implements EnumCode { - CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { - @NonNull - @Override - public Fragment newInstance() { - return ContributionsFragment.newInstance(); - } - }, - NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return NearbyParentFragment.newInstance(); - } - }, - EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { - @NonNull - @Override - public Fragment newInstance() { - return ExploreFragment.newInstance(); - } - }, - BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { - @NonNull - @Override - public Fragment newInstance() { - return BookmarkFragment.newInstance(); - } - }, - MORE(R.string.more, R.drawable.ic_menu_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return null; - } - }; - - private static final EnumCodeMap MAP = new EnumCodeMap<>(NavTab.class); - - @StringRes - private final int text; - @DrawableRes - private final int icon; - - @NonNull - public static NavTab of(int code) { - return MAP.get(code); - } - - public static int size() { - return MAP.size(); - } - - @StringRes - public int text() { - return text; - } - - @DrawableRes - public int icon() { - return icon; - } - - @NonNull - public abstract Fragment newInstance(); - - @Override - public int code() { - // This enumeration is not marshalled so tying declaration order to presentation order is - // convenient and consistent. - return ordinal(); - } - - NavTab(@StringRes int text, @DrawableRes int icon) { - this.text = text; - this.icon = icon; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt new file mode 100644 index 0000000000..4573fccadb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTab.kt @@ -0,0 +1,79 @@ +package fr.free.nrw.commons.navtab + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment + +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap + +import fr.free.nrw.commons.R + + +enum class NavTab( + @StringRes private val text: Int, + @DrawableRes private val icon: Int +) : EnumCode { + + CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) { + override fun newInstance(): Fragment { + return ContributionsFragment.newInstance() + } + }, + NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) { + override fun newInstance(): Fragment { + return NearbyParentFragment.newInstance() + } + }, + EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { + override fun newInstance(): Fragment { + return ExploreFragment.newInstance() + } + }, + BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { + override fun newInstance(): Fragment { + return BookmarkFragment.newInstance() + } + }, + MORE(R.string.more, R.drawable.ic_menu_black_24dp) { + override fun newInstance(): Fragment? { + return null + } + }; + + companion object { + private val MAP: EnumCodeMap = EnumCodeMap(NavTab::class.java) + + @JvmStatic + fun of(code: Int): NavTab { + return MAP[code] + } + + @JvmStatic + fun size(): Int { + return MAP.size() + } + } + + @StringRes + fun text(): Int { + return text + } + + @DrawableRes + fun icon(): Int { + return icon + } + + abstract fun newInstance(): Fragment? + + override fun code(): Int { + // This enumeration is not marshalled so tying declaration order to presentation order is + // convenient and consistent. + return ordinal + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java deleted file mode 100644 index 5384f2e01c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.view.ViewGroup; - -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; - -public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter { - - private Fragment currentFragment; - - public NavTabFragmentPagerAdapter(FragmentManager mgr) { - super(mgr); - } - - @Nullable - public Fragment getCurrentFragment() { - return currentFragment; - } - - @Override - public Fragment getItem(int pos) { - return NavTab.of(pos).newInstance(); - } - - @Override - public int getCount() { - return NavTab.size(); - } - - @Override - public void setPrimaryItem(ViewGroup container, int position, Object object) { - currentFragment = ((Fragment) object); - super.setPrimaryItem(container, position, object); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt new file mode 100644 index 0000000000..369c39ed69 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabFragmentPagerAdapter.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.navtab + +import android.view.ViewGroup + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentPagerAdapter + + +class NavTabFragmentPagerAdapter( + mgr: FragmentManager +) : FragmentPagerAdapter(mgr, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + private var currentFragment: Fragment? = null + + fun getCurrentFragment(): Fragment? { + return currentFragment + } + + override fun getItem(pos: Int): Fragment { + return NavTab.of(pos).newInstance()!! + } + + override fun getCount(): Int { + return NavTab.size() + } + + override fun setPrimaryItem( + container: ViewGroup, + position: Int, + `object`: Any + ) { + currentFragment = `object` as Fragment + super.setPrimaryItem(container, position, `object`) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java deleted file mode 100644 index 399cbc7891..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.Menu; - -import com.google.android.material.bottomnavigation.BottomNavigationView; -import fr.free.nrw.commons.contributions.MainActivity; - - -public class NavTabLayout extends BottomNavigationView { - - public NavTabLayout(Context context) { - super(context); - setTabViews(); - } - - public NavTabLayout(Context context, AttributeSet attrs) { - super(context, attrs); - setTabViews(); - } - - public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - setTabViews(); - } - - private void setTabViews() { - if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) { - for (int i = 0; i < NavTabLoggedOut.size(); i++) { - NavTabLoggedOut navTab = NavTabLoggedOut.of(i); - getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); - } - } else { - for (int i = 0; i < NavTab.size(); i++) { - NavTab navTab = NavTab.of(i); - getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()); - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt new file mode 100644 index 0000000000..8d5298cacd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLayout.kt @@ -0,0 +1,47 @@ +package fr.free.nrw.commons.navtab + +import android.content.Context +import android.util.AttributeSet +import android.view.Menu + +import com.google.android.material.bottomnavigation.BottomNavigationView +import fr.free.nrw.commons.contributions.MainActivity + + +class NavTabLayout : BottomNavigationView { + + constructor(context: Context) : super(context) { + setTabViews() + } + + constructor( + context: Context, + attrs: AttributeSet? + ) : super(context, attrs) { + setTabViews() + } + + constructor( + context: Context, + attrs: AttributeSet?, + defStyleAttr: Int + ) : super(context, attrs, defStyleAttr) { + setTabViews() + } + + private fun setTabViews() { + val isLoginSkipped = (context as MainActivity) + .applicationKvStore.getBoolean("login_skipped") + if (isLoginSkipped) { + for (i in 0 until NavTabLoggedOut.size()) { + val navTab = NavTabLoggedOut.of(i) + menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) + } + } else { + for (i in 0 until NavTab.size()) { + val navTab = NavTab.of(i) + menu.add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon()) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java deleted file mode 100644 index dc1c7ce6bd..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.java +++ /dev/null @@ -1,79 +0,0 @@ -package fr.free.nrw.commons.navtab; - -import androidx.annotation.DrawableRes; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.fragment.app.Fragment; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.wikidata.model.EnumCode; -import fr.free.nrw.commons.wikidata.model.EnumCodeMap; - - -public enum NavTabLoggedOut implements EnumCode { - - EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { - @NonNull - @Override - public Fragment newInstance() { - return ExploreFragment.newInstance(); - } - }, - BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { - @NonNull - @Override - public Fragment newInstance() { - return BookmarkFragment.newInstance(); - } - }, - MORE(R.string.more, R.drawable.ic_menu_black_24dp) { - @NonNull - @Override - public Fragment newInstance() { - return null; - } - }; - - private static final EnumCodeMap MAP = new EnumCodeMap<>( - NavTabLoggedOut.class); - - @StringRes - private final int text; - @DrawableRes - private final int icon; - - @NonNull - public static NavTabLoggedOut of(int code) { - return MAP.get(code); - } - - public static int size() { - return MAP.size(); - } - - @StringRes - public int text() { - return text; - } - - @DrawableRes - public int icon() { - return icon; - } - - @NonNull - public abstract Fragment newInstance(); - - @Override - public int code() { - // This enumeration is not marshalled so tying declaration order to presentation order is - // convenient and consistent. - return ordinal(); - } - - NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) { - this.text = text; - this.icon = icon; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt new file mode 100644 index 0000000000..ad73f1bbd9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/navtab/NavTabLoggedOut.kt @@ -0,0 +1,65 @@ +package fr.free.nrw.commons.navtab + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import fr.free.nrw.commons.R +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.wikidata.model.EnumCode +import fr.free.nrw.commons.wikidata.model.EnumCodeMap + + +enum class NavTabLoggedOut( + @StringRes private val text: Int, + @DrawableRes private val icon: Int +) : EnumCode { + + EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) { + override fun newInstance(): Fragment { + return ExploreFragment.newInstance() + } + }, + BOOKMARKS(R.string.bookmarks, R.drawable.ic_round_star_border_24px) { + override fun newInstance(): Fragment { + return BookmarkFragment.newInstance() + } + }, + MORE(R.string.more, R.drawable.ic_menu_black_24dp) { + override fun newInstance(): Fragment? { + return null + } + }; + + companion object { + private val MAP: EnumCodeMap = EnumCodeMap(NavTabLoggedOut::class.java) + + @JvmStatic + fun of(code: Int): NavTabLoggedOut { + return MAP[code] + } + + @JvmStatic + fun size(): Int { + return MAP.size() + } + } + + @StringRes + fun text(): Int { + return text + } + + @DrawableRes + fun icon(): Int { + return icon + } + + abstract fun newInstance(): Fragment? + + override fun code(): Int { + // This enumeration is not marshalled so tying declaration order to presentation order is + // convenient and consistent. + return ordinal + } +} \ No newline at end of file From 0c969c365bd7ed45e1ca08e9acb8af5b54382ae4 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 02:09:25 -0600 Subject: [PATCH 037/231] Convert auth package to kotlin (#5966) * Convert SessionManager to kotlin along with other small fixes * Convert WikiAccountAuthenticator to kotlin * Migrate WikiAccountAuthenticatorService to kotlin * Converted AccountUtil to kotlin * Convert SignupActivity to kotlin * Convert LoginActivity to kotlin * Merge from main --- .../ui/PasteSensitiveTextInputEditTextTest.kt | 2 +- .../fr/free/nrw/commons/auth/AccountUtil.java | 44 -- .../fr/free/nrw/commons/auth/AccountUtil.kt | 24 + .../free/nrw/commons/auth/LoginActivity.java | 456 ------------------ .../fr/free/nrw/commons/auth/LoginActivity.kt | 404 ++++++++++++++++ .../free/nrw/commons/auth/SessionManager.java | 148 ------ .../free/nrw/commons/auth/SessionManager.kt | 95 ++++ .../free/nrw/commons/auth/SignupActivity.java | 82 ---- .../free/nrw/commons/auth/SignupActivity.kt | 75 +++ .../auth/WikiAccountAuthenticator.java | 141 ------ .../commons/auth/WikiAccountAuthenticator.kt | 108 +++++ .../auth/WikiAccountAuthenticatorService.java | 31 -- .../auth/WikiAccountAuthenticatorService.kt | 22 + .../commons/di/CommonsApplicationModule.java | 6 - .../feedback/FeedbackContentCreator.java | 4 +- .../commons/media/MediaDetailFragment.java | 12 +- .../notification/NotificationActivity.kt | 2 +- .../free/nrw/commons/review/ReviewActivity.kt | 4 +- .../commons/upload/FailedUploadsFragment.kt | 2 +- .../nrw/commons/utils/AbstractTextWatcher.kt | 2 +- .../nrw/commons/TestCommonsApplication.kt | 4 - .../nrw/commons/auth/AccountUtilUnitTest.kt | 16 +- .../commons/auth/LoginActivityUnitTests.kt | 11 - ...WikiAccountAuthenticatorServiceUnitTest.kt | 16 +- .../auth/WikiAccountAuthenticatorUnitTest.kt | 5 +- 25 files changed, 752 insertions(+), 964 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt index aedbcb133b..647c5bbdaa 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/ui/PasteSensitiveTextInputEditTextTest.kt @@ -17,7 +17,7 @@ class PasteSensitiveTextInputEditTextTest { @Before fun setup() { context = ApplicationProvider.getApplicationContext() - textView = PasteSensitiveTextInputEditText(context) + textView = PasteSensitiveTextInputEditText(context!!) } // this test has no real value, just % for test code coverage diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java deleted file mode 100644 index 53903769d6..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.BuildConfig; -import timber.log.Timber; - -public class AccountUtil { - - public static final String AUTH_TOKEN_TYPE = "CommonsAndroid"; - - public AccountUtil() { - } - - /** - * @return Account|null - */ - @Nullable - public static Account account(Context context) { - try { - Account[] accounts = accountManager(context).getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (accounts.length > 0) { - return accounts[0]; - } - } catch (SecurityException e) { - Timber.e(e); - } - return null; - } - - @Nullable - public static String getUserName(Context context) { - Account account = account(context); - return account == null ? null : account.name; - } - - private static AccountManager accountManager(Context context) { - return AccountManager.get(context); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt new file mode 100644 index 0000000000..aa86cd0d85 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/AccountUtil.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import androidx.annotation.VisibleForTesting +import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE +import timber.log.Timber + +const val AUTH_TOKEN_TYPE: String = "CommonsAndroid" + +fun getUserName(context: Context): String? { + return account(context)?.name +} + +@VisibleForTesting +fun account(context: Context): Account? = try { + val accountManager = AccountManager.get(context) + val accounts = accountManager.getAccountsByType(ACCOUNT_TYPE) + if (accounts.isNotEmpty()) accounts[0] else null +} catch (e: SecurityException) { + Timber.e(e) + null +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java deleted file mode 100644 index 3ff61e511c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ /dev/null @@ -1,456 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AccountAuthenticatorActivity; -import android.app.ProgressDialog; -import android.content.Context; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.KeyEvent; -import android.view.MenuInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; - -import android.widget.TextView; -import androidx.annotation.ColorRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringRes; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.app.NavUtils; -import androidx.core.content.ContextCompat; -import fr.free.nrw.commons.auth.login.LoginClient; -import fr.free.nrw.commons.auth.login.LoginResult; -import fr.free.nrw.commons.databinding.ActivityLoginBinding; -import fr.free.nrw.commons.utils.ActivityUtils; -import java.util.Locale; -import fr.free.nrw.commons.auth.login.LoginCallback; - -import java.util.Objects; -import javax.inject.Inject; -import javax.inject.Named; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.disposables.CompositeDisposable; -import timber.log.Timber; - -import static android.view.KeyEvent.KEYCODE_ENTER; -import static android.view.View.VISIBLE; -import static android.view.inputmethod.EditorInfo.IME_ACTION_DONE; -import static fr.free.nrw.commons.CommonsApplication.LOGIN_MESSAGE_INTENT_KEY; -import static fr.free.nrw.commons.CommonsApplication.LOGIN_USERNAME_INTENT_KEY; - -public class LoginActivity extends AccountAuthenticatorActivity { - - @Inject - SessionManager sessionManager; - - @Inject - @Named("default_preferences") - JsonKvStore applicationKvStore; - - @Inject - LoginClient loginClient; - - @Inject - SystemThemeUtils systemThemeUtils; - - private ActivityLoginBinding binding; - ProgressDialog progressDialog; - private AppCompatDelegate delegate; - private LoginTextWatcher textWatcher = new LoginTextWatcher(); - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - final String saveProgressDailog="ProgressDailog_state"; - final String saveErrorMessage ="errorMessage"; - final String saveUsername="username"; - final String savePassword="password"; - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ApplicationlessInjection - .getInstance(this.getApplicationContext()) - .getCommonsApplicationComponent() - .inject(this); - - boolean isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - setTheme(isDarkTheme ? R.style.DarkAppTheme : R.style.LightAppTheme); - getDelegate().installViewFactory(); - getDelegate().onCreate(savedInstanceState); - - binding = ActivityLoginBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - String message = getIntent().getStringExtra(LOGIN_MESSAGE_INTENT_KEY); - String username = getIntent().getStringExtra(LOGIN_USERNAME_INTENT_KEY); - - binding.loginUsername.addTextChangedListener(textWatcher); - binding.loginPassword.addTextChangedListener(textWatcher); - binding.loginTwoFactor.addTextChangedListener(textWatcher); - - binding.skipLogin.setOnClickListener(view -> skipLogin()); - binding.forgotPassword.setOnClickListener(view -> forgotPassword()); - binding.aboutPrivacyPolicy.setOnClickListener(view -> onPrivacyPolicyClicked()); - binding.signUpButton.setOnClickListener(view -> signUp()); - binding.loginButton.setOnClickListener(view -> performLogin()); - - binding.loginPassword.setOnEditorActionListener(this::onEditorAction); - binding.loginPassword.setOnFocusChangeListener(this::onPasswordFocusChanged); - - if (ConfigUtils.isBetaFlavour()) { - binding.loginCredentials.setText(getString(R.string.login_credential)); - } else { - binding.loginCredentials.setVisibility(View.GONE); - } - if (message != null) { - showMessage(message, R.color.secondaryDarkColor); - } - if (username != null) { - binding.loginUsername.setText(username); - } - } - /** - * Hides the keyboard if the user's focus is not on the password (hasFocus is false). - * @param view The keyboard - * @param hasFocus Set to true if the keyboard has focus - */ - void onPasswordFocusChanged(View view, boolean hasFocus) { - if (!hasFocus) { - ViewUtil.hideKeyboard(view); - } - } - - boolean onEditorAction(TextView textView, int actionId, KeyEvent keyEvent) { - if (binding.loginButton.isEnabled()) { - if (actionId == IME_ACTION_DONE) { - performLogin(); - return true; - } else if ((keyEvent != null) && keyEvent.getKeyCode() == KEYCODE_ENTER) { - performLogin(); - return true; - } - } - return false; - } - - - protected void skipLogin() { - new AlertDialog.Builder(this).setTitle(R.string.skip_login_title) - .setMessage(R.string.skip_login_message) - .setCancelable(false) - .setPositiveButton(R.string.yes, (dialog, which) -> { - dialog.cancel(); - performSkipLogin(); - }) - .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) - .show(); - } - - protected void forgotPassword() { - Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)); - } - - protected void onPrivacyPolicyClicked() { - Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)); - } - - protected void signUp() { - Intent intent = new Intent(this, SignupActivity.class); - startActivity(intent); - } - - @Override - protected void onPostCreate(Bundle savedInstanceState) { - super.onPostCreate(savedInstanceState); - getDelegate().onPostCreate(savedInstanceState); - } - - @Override - protected void onResume() { - super.onResume(); - - if (sessionManager.getCurrentAccount() != null - && sessionManager.isUserLoggedIn()) { - applicationKvStore.putBoolean("login_skipped", false); - startMainActivity(); - } - - if (applicationKvStore.getBoolean("login_skipped", false)) { - performSkipLogin(); - } - - } - - @Override - protected void onDestroy() { - compositeDisposable.clear(); - try { - // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method - if (progressDialog != null && progressDialog.isShowing()) { - progressDialog.dismiss(); - } - } catch (Exception e) { - e.printStackTrace(); - } - binding.loginUsername.removeTextChangedListener(textWatcher); - binding.loginPassword.removeTextChangedListener(textWatcher); - binding.loginTwoFactor.removeTextChangedListener(textWatcher); - delegate.onDestroy(); - if(null!=loginClient) { - loginClient.cancel(); - } - binding = null; - super.onDestroy(); - } - - public void performLogin() { - Timber.d("Login to start!"); - final String username = Objects.requireNonNull(binding.loginUsername.getText()).toString(); - final String password = Objects.requireNonNull(binding.loginPassword.getText()).toString(); - final String twoFactorCode = Objects.requireNonNull(binding.loginTwoFactor.getText()).toString(); - - showLoggingProgressBar(); - loginClient.doLogin(username, password, twoFactorCode, Locale.getDefault().getLanguage(), - new LoginCallback() { - @Override - public void success(@NonNull LoginResult loginResult) { - runOnUiThread(()->{ - Timber.d("Login Success"); - hideProgress(); - onLoginSuccess(loginResult); - }); - } - - @Override - public void twoFactorPrompt(@NonNull Throwable caught, @Nullable String token) { - runOnUiThread(()->{ - Timber.d("Requesting 2FA prompt"); - hideProgress(); - askUserForTwoFactorAuth(); - }); - } - - @Override - public void passwordResetPrompt(@Nullable String token) { - runOnUiThread(()->{ - Timber.d("Showing password reset prompt"); - hideProgress(); - showPasswordResetPrompt(); - }); - } - - @Override - public void error(@NonNull Throwable caught) { - runOnUiThread(()->{ - Timber.e(caught); - hideProgress(); - showMessageAndCancelDialog(caught.getLocalizedMessage()); - }); - } - }); - } - - - - private void hideProgress() { - progressDialog.dismiss(); - } - - private void showPasswordResetPrompt() { - showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)); - } - - - /** - * This function is called when user skips the login. - * It redirects the user to Explore Activity. - */ - private void performSkipLogin() { - applicationKvStore.putBoolean("login_skipped", true); - MainActivity.startYourself(this); - finish(); - } - - private void showLoggingProgressBar() { - progressDialog = new ProgressDialog(this); - progressDialog.setIndeterminate(true); - progressDialog.setTitle(getString(R.string.logging_in_title)); - progressDialog.setMessage(getString(R.string.logging_in_message)); - progressDialog.setCanceledOnTouchOutside(false); - progressDialog.show(); - } - - private void onLoginSuccess(LoginResult loginResult) { - compositeDisposable.clear(); - sessionManager.setUserLoggedIn(true); - sessionManager.updateAccount(loginResult); - progressDialog.dismiss(); - showSuccessAndDismissDialog(); - startMainActivity(); - } - - @Override - protected void onStart() { - super.onStart(); - delegate.onStart(); - } - - @Override - protected void onStop() { - super.onStop(); - delegate.onStop(); - } - - @Override - protected void onPostResume() { - super.onPostResume(); - getDelegate().onPostResume(); - } - - @Override - public void setContentView(View view, ViewGroup.LayoutParams params) { - getDelegate().setContentView(view, params); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - NavUtils.navigateUpFromSameTask(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - @NonNull - public MenuInflater getMenuInflater() { - return getDelegate().getMenuInflater(); - } - - public void askUserForTwoFactorAuth() { - progressDialog.dismiss(); - binding.twoFactorContainer.setVisibility(VISIBLE); - binding.loginTwoFactor.setVisibility(VISIBLE); - binding.loginTwoFactor.requestFocus(); - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY); - showMessageAndCancelDialog(R.string.login_failed_2fa_needed); - } - - public void showMessageAndCancelDialog(@StringRes int resId) { - showMessage(resId, R.color.secondaryDarkColor); - if (progressDialog != null) { - progressDialog.cancel(); - } - } - - public void showMessageAndCancelDialog(String error) { - showMessage(error, R.color.secondaryDarkColor); - if (progressDialog != null) { - progressDialog.cancel(); - } - } - - public void showSuccessAndDismissDialog() { - showMessage(R.string.login_success, R.color.primaryDarkColor); - progressDialog.dismiss(); - } - - public void startMainActivity() { - ActivityUtils.startActivityWithFlags(this, MainActivity.class, Intent.FLAG_ACTIVITY_SINGLE_TOP); - finish(); - } - - private void showMessage(@StringRes int resId, @ColorRes int colorResId) { - binding.errorMessage.setText(getString(resId)); - binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - binding.errorMessageContainer.setVisibility(VISIBLE); - } - - private void showMessage(String message, @ColorRes int colorResId) { - binding.errorMessage.setText(message); - binding.errorMessage.setTextColor(ContextCompat.getColor(this, colorResId)); - binding.errorMessageContainer.setVisibility(VISIBLE); - } - - private AppCompatDelegate getDelegate() { - if (delegate == null) { - delegate = AppCompatDelegate.create(this, null); - } - return delegate; - } - - private class LoginTextWatcher implements TextWatcher { - @Override - public void beforeTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence charSequence, int start, int count, int after) { - } - - @Override - public void afterTextChanged(Editable editable) { - boolean enabled = binding.loginUsername.getText().length() != 0 && - binding.loginPassword.getText().length() != 0 && - (BuildConfig.DEBUG || binding.loginTwoFactor.getText().length() != 0 || - binding.loginTwoFactor.getVisibility() != VISIBLE); - binding.loginButton.setEnabled(enabled); - } - } - - public static void startYourself(Context context) { - Intent intent = new Intent(context, LoginActivity.class); - context.startActivity(intent); - } - - @Override - protected void onSaveInstanceState(Bundle outState) { - // if progressDialog is visible during the configuration change then store state as true else false so that - // we maintain visibility of progressDailog after configuration change - if(progressDialog!=null&&progressDialog.isShowing()) { - outState.putBoolean(saveProgressDailog,true); - } else { - outState.putBoolean(saveProgressDailog,false); - } - outState.putString(saveErrorMessage,binding.errorMessage.getText().toString()); //Save the errorMessage - outState.putString(saveUsername,getUsername()); // Save the username - outState.putString(savePassword,getPassword()); // Save the password - } - private String getUsername() { - return binding.loginUsername.getText().toString(); - } - private String getPassword(){ - return binding.loginPassword.getText().toString(); - } - - @Override - protected void onRestoreInstanceState(final Bundle savedInstanceState) { - super.onRestoreInstanceState(savedInstanceState); - binding.loginUsername.setText(savedInstanceState.getString(saveUsername)); - binding.loginPassword.setText(savedInstanceState.getString(savePassword)); - if(savedInstanceState.getBoolean(saveProgressDailog)) { - performLogin(); - } - String errorMessage=savedInstanceState.getString(saveErrorMessage); - if(sessionManager.isUserLoggedIn()) { - showMessage(R.string.login_success, R.color.primaryDarkColor); - } else { - showMessage(errorMessage, R.color.secondaryDarkColor); - } - } -} 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 new file mode 100644 index 0000000000..3aa9b26d95 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.kt @@ -0,0 +1,404 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AccountAuthenticatorActivity +import android.app.ProgressDialog +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.KeyEvent +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputMethodManager +import android.widget.TextView +import androidx.annotation.ColorRes +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.app.NavUtils +import androidx.core.content.ContextCompat +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.login.LoginCallback +import fr.free.nrw.commons.auth.login.LoginClient +import fr.free.nrw.commons.auth.login.LoginResult +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.ActivityLoginBinding +import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.utils.AbstractTextWatcher +import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.SystemThemeUtils +import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard +import io.reactivex.disposables.CompositeDisposable +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + +class LoginActivity : AccountAuthenticatorActivity() { + @Inject + lateinit var sessionManager: SessionManager + + @Inject + @field:Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + + @Inject + lateinit var loginClient: LoginClient + + @Inject + lateinit var systemThemeUtils: SystemThemeUtils + + private var binding: ActivityLoginBinding? = null + private var progressDialog: ProgressDialog? = null + private val textWatcher = AbstractTextWatcher(::onTextChanged) + private val compositeDisposable = CompositeDisposable() + private val delegate: AppCompatDelegate by lazy { + AppCompatDelegate.create(this, null) + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + ApplicationlessInjection + .getInstance(this.applicationContext) + .commonsApplicationComponent + .inject(this) + + val isDarkTheme = systemThemeUtils.isDeviceInNightMode() + setTheme(if (isDarkTheme) R.style.DarkAppTheme else R.style.LightAppTheme) + delegate.installViewFactory() + delegate.onCreate(savedInstanceState) + + binding = ActivityLoginBinding.inflate(layoutInflater) + with(binding!!) { + setContentView(root) + + loginUsername.addTextChangedListener(textWatcher) + loginPassword.addTextChangedListener(textWatcher) + loginTwoFactor.addTextChangedListener(textWatcher) + + skipLogin.setOnClickListener { skipLogin() } + forgotPassword.setOnClickListener { forgotPassword() } + aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() } + signUpButton.setOnClickListener { signUp() } + loginButton.setOnClickListener { performLogin() } + loginPassword.setOnEditorActionListener(::onEditorAction) + + loginPassword.onFocusChangeListener = + View.OnFocusChangeListener(::onPasswordFocusChanged) + + if (isBetaFlavour) { + loginCredentials.text = getString(R.string.login_credential) + } else { + loginCredentials.visibility = View.GONE + } + + intent.getStringExtra(CommonsApplication.LOGIN_MESSAGE_INTENT_KEY)?.let { + showMessage(it, R.color.secondaryDarkColor) + } + + intent.getStringExtra(CommonsApplication.LOGIN_USERNAME_INTENT_KEY)?.let { + loginUsername.setText(it) + } + } + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + delegate.onPostCreate(savedInstanceState) + } + + override fun onResume() { + super.onResume() + + if (sessionManager.currentAccount != null && sessionManager.isUserLoggedIn) { + applicationKvStore.putBoolean("login_skipped", false) + startMainActivity() + } + + if (applicationKvStore.getBoolean("login_skipped", false)) { + performSkipLogin() + } + } + + override fun onDestroy() { + compositeDisposable.clear() + try { + // To prevent leaked window when finish() is called, see http://stackoverflow.com/questions/32065854/activity-has-leaked-window-at-alertdialog-show-method + if (progressDialog?.isShowing == true) { + progressDialog!!.dismiss() + } + } catch (e: Exception) { + e.printStackTrace() + } + with(binding!!) { + loginUsername.removeTextChangedListener(textWatcher) + loginPassword.removeTextChangedListener(textWatcher) + loginTwoFactor.removeTextChangedListener(textWatcher) + } + delegate.onDestroy() + loginClient?.cancel() + binding = null + super.onDestroy() + } + + override fun onStart() { + super.onStart() + delegate.onStart() + } + + override fun onStop() { + super.onStop() + delegate.onStop() + } + + override fun onPostResume() { + super.onPostResume() + delegate.onPostResume() + } + + override fun setContentView(view: View, params: ViewGroup.LayoutParams) { + delegate.setContentView(view, params) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + NavUtils.navigateUpFromSameTask(this) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onSaveInstanceState(outState: Bundle) { + // if progressDialog is visible during the configuration change then store state as true else false so that + // we maintain visibility of progressDailog after configuration change + if (progressDialog != null && progressDialog!!.isShowing) { + outState.putBoolean(saveProgressDailog, true) + } else { + outState.putBoolean(saveProgressDailog, false) + } + outState.putString( + saveErrorMessage, + binding!!.errorMessage.text.toString() + ) //Save the errorMessage + outState.putString( + saveUsername, + binding!!.loginUsername.text.toString() + ) // Save the username + outState.putString( + savePassword, + binding!!.loginPassword.text.toString() + ) // Save the password + } + + override fun onRestoreInstanceState(savedInstanceState: Bundle) { + super.onRestoreInstanceState(savedInstanceState) + binding!!.loginUsername.setText(savedInstanceState.getString(saveUsername)) + binding!!.loginPassword.setText(savedInstanceState.getString(savePassword)) + if (savedInstanceState.getBoolean(saveProgressDailog)) { + performLogin() + } + val errorMessage = savedInstanceState.getString(saveErrorMessage) + if (sessionManager.isUserLoggedIn) { + showMessage(R.string.login_success, R.color.primaryDarkColor) + } else { + showMessage(errorMessage, R.color.secondaryDarkColor) + } + } + + /** + * Hides the keyboard if the user's focus is not on the password (hasFocus is false). + * @param view The keyboard + * @param hasFocus Set to true if the keyboard has focus + */ + private fun onPasswordFocusChanged(view: View, hasFocus: Boolean) { + if (!hasFocus) { + hideKeyboard(view) + } + } + + private fun onEditorAction(textView: TextView, actionId: Int, keyEvent: KeyEvent?) = + if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) { + performLogin() + true + } else false + + private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) = + actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER + + private fun skipLogin() { + AlertDialog.Builder(this) + .setTitle(R.string.skip_login_title) + .setMessage(R.string.skip_login_message) + .setCancelable(false) + .setPositiveButton(R.string.yes) { dialog: DialogInterface, which: Int -> + dialog.cancel() + performSkipLogin() + } + .setNegativeButton(R.string.no) { dialog: DialogInterface, which: Int -> + dialog.cancel() + } + .show() + } + + private fun forgotPassword() = + Utils.handleWebUrl(this, Uri.parse(BuildConfig.FORGOT_PASSWORD_URL)) + + private fun onPrivacyPolicyClicked() = + Utils.handleWebUrl(this, Uri.parse(BuildConfig.PRIVACY_POLICY_URL)) + + private fun signUp() = + startActivity(Intent(this, SignupActivity::class.java)) + + @VisibleForTesting + fun performLogin() { + Timber.d("Login to start!") + val username = binding!!.loginUsername.text.toString() + val password = binding!!.loginPassword.text.toString() + val twoFactorCode = binding!!.loginTwoFactor.text.toString() + + showLoggingProgressBar() + loginClient.doLogin(username, + password, + twoFactorCode, + Locale.getDefault().language, + object : LoginCallback { + override fun success(loginResult: LoginResult) = runOnUiThread { + Timber.d("Login Success") + progressDialog!!.dismiss() + onLoginSuccess(loginResult) + } + + override fun twoFactorPrompt(caught: Throwable, token: String?) = runOnUiThread { + Timber.d("Requesting 2FA prompt") + progressDialog!!.dismiss() + askUserForTwoFactorAuth() + } + + override fun passwordResetPrompt(token: String?) = runOnUiThread { + Timber.d("Showing password reset prompt") + progressDialog!!.dismiss() + showPasswordResetPrompt() + } + + override fun error(caught: Throwable) = runOnUiThread { + Timber.e(caught) + progressDialog!!.dismiss() + showMessageAndCancelDialog(caught.localizedMessage ?: "") + } + } + ) + } + + private fun showPasswordResetPrompt() = + showMessageAndCancelDialog(getString(R.string.you_must_reset_your_passsword)) + + /** + * This function is called when user skips the login. + * It redirects the user to Explore Activity. + */ + private fun performSkipLogin() { + applicationKvStore.putBoolean("login_skipped", true) + MainActivity.startYourself(this) + finish() + } + + private fun showLoggingProgressBar() { + progressDialog = ProgressDialog(this).apply { + isIndeterminate = true + setTitle(getString(R.string.logging_in_title)) + setMessage(getString(R.string.logging_in_message)) + setCanceledOnTouchOutside(false) + } + progressDialog!!.show() + } + + private fun onLoginSuccess(loginResult: LoginResult) { + compositeDisposable.clear() + sessionManager.setUserLoggedIn(true) + sessionManager.updateAccount(loginResult) + progressDialog!!.dismiss() + showSuccessAndDismissDialog() + startMainActivity() + } + + override fun getMenuInflater(): MenuInflater = + delegate.menuInflater + + @VisibleForTesting + fun askUserForTwoFactorAuth() { + progressDialog!!.dismiss() + with(binding!!) { + twoFactorContainer.visibility = View.VISIBLE + 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) + } + + @VisibleForTesting + fun showMessageAndCancelDialog(@StringRes resId: Int) { + showMessage(resId, R.color.secondaryDarkColor) + progressDialog?.cancel() + } + + @VisibleForTesting + fun showMessageAndCancelDialog(error: String) { + showMessage(error, R.color.secondaryDarkColor) + progressDialog?.cancel() + } + + @VisibleForTesting + fun showSuccessAndDismissDialog() { + showMessage(R.string.login_success, R.color.primaryDarkColor) + progressDialog!!.dismiss() + } + + @VisibleForTesting + fun startMainActivity() { + startActivityWithFlags(this, MainActivity::class.java, Intent.FLAG_ACTIVITY_SINGLE_TOP) + finish() + } + + private fun showMessage(@StringRes resId: Int, @ColorRes colorResId: Int) = with(binding!!) { + errorMessage.text = getString(resId) + errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) + errorMessageContainer.visibility = View.VISIBLE + } + + private fun showMessage(message: String?, @ColorRes colorResId: Int) = with(binding!!) { + errorMessage.text = message + errorMessage.setTextColor(ContextCompat.getColor(this@LoginActivity, colorResId)) + errorMessageContainer.visibility = View.VISIBLE + } + + private fun onTextChanged(text: String) { + val enabled = + binding!!.loginUsername.text!!.length != 0 && binding!!.loginPassword.text!!.length != 0 && + (BuildConfig.DEBUG || binding!!.loginTwoFactor.text!!.length != 0 || binding!!.loginTwoFactor.visibility != View.VISIBLE) + binding!!.loginButton.isEnabled = enabled + } + + companion object { + fun startYourself(context: Context) = + context.startActivity(Intent(context, LoginActivity::class.java)) + + const val saveProgressDailog: String = "ProgressDailog_state" + const val saveErrorMessage: String = "errorMessage" + const val saveUsername: String = "username" + const val savePassword: String = "password" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java deleted file mode 100644 index 7c2f4a3348..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ /dev/null @@ -1,148 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.os.Build; -import android.text.TextUtils; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.auth.login.LoginResult; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import io.reactivex.Completable; -import io.reactivex.Observable; - -/** - * Manage the current logged in user session. - */ -@Singleton -public class SessionManager { - private final Context context; - private Account currentAccount; // Unlike a savings account... ;-) - private JsonKvStore defaultKvStore; - - @Inject - public SessionManager(Context context, - @Named("default_preferences") JsonKvStore defaultKvStore) { - this.context = context; - this.currentAccount = null; - this.defaultKvStore = defaultKvStore; - } - - private boolean createAccount(@NonNull String userName, @NonNull String password) { - Account account = getCurrentAccount(); - if (account == null || TextUtils.isEmpty(account.name) || !account.name.equals(userName)) { - removeAccount(); - account = new Account(userName, BuildConfig.ACCOUNT_TYPE); - return accountManager().addAccountExplicitly(account, password, null); - } - return true; - } - - private void removeAccount() { - Account account = getCurrentAccount(); - if (account != null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - accountManager().removeAccountExplicitly(account); - } else { - //noinspection deprecation - accountManager().removeAccount(account, null, null); - } - } - } - - public void updateAccount(LoginResult result) { - boolean accountCreated = createAccount(result.getUserName(), result.getPassword()); - if (accountCreated) { - setPassword(result.getPassword()); - } - } - - private void setPassword(@NonNull String password) { - Account account = getCurrentAccount(); - if (account != null) { - accountManager().setPassword(account, password); - } - } - - /** - * @return Account|null - */ - @Nullable - public Account getCurrentAccount() { - if (currentAccount == null) { - AccountManager accountManager = AccountManager.get(context); - Account[] allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - currentAccount = allAccounts[0]; - } - } - return currentAccount; - } - - public boolean doesAccountExist() { - return getCurrentAccount() != null; - } - - @Nullable - public String getUserName() { - Account account = getCurrentAccount(); - return account == null ? null : account.name; - } - - @Nullable - public String getPassword() { - Account account = getCurrentAccount(); - return account == null ? null : accountManager().getPassword(account); - } - - private AccountManager accountManager() { - return AccountManager.get(context); - } - - public boolean isUserLoggedIn() { - return defaultKvStore.getBoolean("isUserLoggedIn", false); - } - - void setUserLoggedIn(boolean isLoggedIn) { - defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn); - } - - public void forceLogin(Context context) { - if (context != null) { - LoginActivity.startYourself(context); - } - } - - /** - * Returns a Completable that clears existing accounts from account manager - */ - public Completable logout() { - return Completable.fromObservable( - Observable.empty() - .doOnComplete( - () -> { - removeAccount(); - currentAccount = null; - } - ) - ); - } - - /** - * Return a corresponding boolean preference - * - * @param key - * @return - */ - public boolean getPreference(String key) { - return defaultKvStore.getBoolean(key); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt new file mode 100644 index 0000000000..eba4a55f41 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt @@ -0,0 +1,95 @@ +package fr.free.nrw.commons.auth + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import android.os.Build +import android.text.TextUtils +import fr.free.nrw.commons.BuildConfig.ACCOUNT_TYPE +import fr.free.nrw.commons.auth.login.LoginResult +import fr.free.nrw.commons.kvstore.JsonKvStore +import io.reactivex.Completable +import io.reactivex.Observable +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * Manage the current logged in user session. + */ +@Singleton +class SessionManager @Inject constructor( + private val context: Context, + @param:Named("default_preferences") private val defaultKvStore: JsonKvStore +) { + private val accountManager: AccountManager get() = AccountManager.get(context) + + private var _currentAccount: Account? = null // Unlike a savings account... ;-) + val currentAccount: Account? get() { + if (_currentAccount == null) { + val allAccounts = AccountManager.get(context).getAccountsByType(ACCOUNT_TYPE) + if (allAccounts.isNotEmpty()) { + _currentAccount = allAccounts[0] + } + } + return _currentAccount + } + + val userName: String? + get() = currentAccount?.name + + var password: String? + get() = currentAccount?.let { accountManager.getPassword(it) } + private set(value) { + currentAccount?.let { accountManager.setPassword(it, value) } + } + + val isUserLoggedIn: Boolean + get() = defaultKvStore.getBoolean("isUserLoggedIn", false) + + fun updateAccount(result: LoginResult) { + if (createAccount(result.userName!!, result.password!!)) { + password = result.password + } + } + + fun doesAccountExist(): Boolean = + currentAccount != null + + fun setUserLoggedIn(isLoggedIn: Boolean) = + defaultKvStore.putBoolean("isUserLoggedIn", isLoggedIn) + + fun forceLogin(context: Context?) = + context?.let { LoginActivity.startYourself(it) } + + fun getPreference(key: String?): Boolean = + defaultKvStore.getBoolean(key) + + fun logout(): Completable = Completable.fromObservable( + Observable.empty() + .doOnComplete { + removeAccount() + _currentAccount = null + } + ) + + private fun createAccount(userName: String, password: String): Boolean { + var account = currentAccount + if (account == null || TextUtils.isEmpty(account.name) || account.name != userName) { + removeAccount() + account = Account(userName, ACCOUNT_TYPE) + return accountManager.addAccountExplicitly(account, password, null) + } + return true + } + + private fun removeAccount() { + currentAccount?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { + accountManager.removeAccountExplicitly(it) + } else { + accountManager.removeAccount(it, null, null) + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java deleted file mode 100644 index be90bb4bb6..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.java +++ /dev/null @@ -1,82 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.webkit.WebSettings; -import android.webkit.WebView; -import android.webkit.WebViewClient; -import android.widget.Toast; - -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.theme.BaseActivity; -import timber.log.Timber; - -public class SignupActivity extends BaseActivity { - - private WebView webView; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Timber.d("Signup Activity started"); - - webView = new WebView(this); - setContentView(webView); - - webView.setWebViewClient(new MyWebViewClient()); - WebSettings webSettings = webView.getSettings(); - /*Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can - trust Wikimedia's site... right?*/ - webSettings.setJavaScriptEnabled(true); - - webView.loadUrl(BuildConfig.SIGNUP_LANDING_URL); - } - - private class MyWebViewClient extends WebViewClient { - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (url.equals(BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL)) { - //Signup success, so clear cookies, notify user, and load LoginActivity again - Timber.d("Overriding URL %s", url); - - Toast toast = Toast.makeText(SignupActivity.this, - R.string.account_created, Toast.LENGTH_LONG); - toast.show(); - // terminate on task completion. - finish(); - return true; - } else { - //If user clicks any other links in the webview - Timber.d("Not overriding URL, URL is: %s", url); - return false; - } - } - } - - @Override - public void onBackPressed() { - if (webView.canGoBack()) { - webView.goBack(); - } else { - super.onBackPressed(); - } - } - - /** - * Known bug in androidx.appcompat library version 1.1.0 being tracked here - * https://issuetracker.google.com/issues/141132133 - * App tries to put light/dark theme to webview and crashes in the process - * This code tries to prevent applying the theme when sdk is between api 21 to 25 - * @param overrideConfiguration - */ - @Override - public void applyOverrideConfiguration(final Configuration overrideConfiguration) { - if (Build.VERSION.SDK_INT <= 25 && - (getResources().getConfiguration().uiMode == getApplicationContext().getResources().getConfiguration().uiMode)) { - return; - } - super.applyOverrideConfiguration(overrideConfiguration); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt new file mode 100644 index 0000000000..5b48ecd8f5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/SignupActivity.kt @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.auth + +import android.annotation.SuppressLint +import android.content.res.Configuration +import android.os.Build +import android.os.Bundle +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Toast +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.theme.BaseActivity +import timber.log.Timber + +class SignupActivity : BaseActivity() { + private var webView: WebView? = null + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Timber.d("Signup Activity started") + + webView = WebView(this) + with(webView!!) { + setContentView(this) + webViewClient = MyWebViewClient() + // Needed to refresh Captcha. Might introduce XSS vulnerabilities, but we can + // trust Wikimedia's site... right? + settings.javaScriptEnabled = true + loadUrl(BuildConfig.SIGNUP_LANDING_URL) + } + } + + override fun onBackPressed() { + if (webView!!.canGoBack()) { + webView!!.goBack() + } else { + super.onBackPressed() + } + } + + /** + * Known bug in androidx.appcompat library version 1.1.0 being tracked here + * https://issuetracker.google.com/issues/141132133 + * App tries to put light/dark theme to webview and crashes in the process + * This code tries to prevent applying the theme when sdk is between api 21 to 25 + */ + override fun applyOverrideConfiguration(overrideConfiguration: Configuration) { + if (Build.VERSION.SDK_INT <= 25 && + (resources.configuration.uiMode == applicationContext.resources.configuration.uiMode) + ) return + super.applyOverrideConfiguration(overrideConfiguration) + } + + private inner class MyWebViewClient : WebViewClient() { + @Deprecated("Deprecated in Java") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean = + if (url == BuildConfig.SIGNUP_SUCCESS_REDIRECTION_URL) { + //Signup success, so clear cookies, notify user, and load LoginActivity again + Timber.d("Overriding URL %s", url) + + Toast.makeText( + this@SignupActivity, R.string.account_created, Toast.LENGTH_LONG + ).show() + + // terminate on task completion. + finish() + true + } else { + //If user clicks any other links in the webview + Timber.d("Not overriding URL, URL is: %s", url) + false + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java deleted file mode 100644 index 6437256040..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.java +++ /dev/null @@ -1,141 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AbstractAccountAuthenticator; -import android.accounts.Account; -import android.accounts.AccountAuthenticatorResponse; -import android.accounts.AccountManager; -import android.accounts.NetworkErrorException; -import android.content.ContentResolver; -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.BuildConfig; - -import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; - -/** - * Handles WikiMedia commons account Authentication - */ -public class WikiAccountAuthenticator extends AbstractAccountAuthenticator { - private static final String[] SYNC_AUTHORITIES = {BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY}; - - @NonNull - private final Context context; - - public WikiAccountAuthenticator(@NonNull Context context) { - super(context); - this.context = context; - } - - /** - * Provides Bundle with edited Account Properties - */ - @Override - public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) { - Bundle bundle = new Bundle(); - bundle.putString("test", "editProperties"); - return bundle; - } - - @Override - public Bundle addAccount(@NonNull AccountAuthenticatorResponse response, - @NonNull String accountType, @Nullable String authTokenType, - @Nullable String[] requiredFeatures, @Nullable Bundle options) - throws NetworkErrorException { - // account type not supported returns bundle without loginActivity Intent, it just contains "test" key - if (!supportedAccountType(accountType)) { - Bundle bundle = new Bundle(); - bundle.putString("test", "addAccount"); - return bundle; - } - - return addAccount(response); - } - - @Override - public Bundle confirmCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "confirmCredentials"); - return bundle; - } - - @Override - public Bundle getAuthToken(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @NonNull String authTokenType, - @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "getAuthToken"); - return bundle; - } - - @Nullable - @Override - public String getAuthTokenLabel(@NonNull String authTokenType) { - return supportedAccountType(authTokenType) ? AUTH_TOKEN_TYPE : null; - } - - @Nullable - @Override - public Bundle updateCredentials(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @Nullable String authTokenType, - @Nullable Bundle options) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putString("test", "updateCredentials"); - return bundle; - } - - @Nullable - @Override - public Bundle hasFeatures(@NonNull AccountAuthenticatorResponse response, - @NonNull Account account, @NonNull String[] features) - throws NetworkErrorException { - Bundle bundle = new Bundle(); - bundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false); - return bundle; - } - - private boolean supportedAccountType(@Nullable String type) { - return BuildConfig.ACCOUNT_TYPE.equals(type); - } - - /** - * Provides a bundle containing a Parcel - * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) - */ - private Bundle addAccount(AccountAuthenticatorResponse response) { - Intent intent = new Intent(context, LoginActivity.class); - intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response); - - Bundle bundle = new Bundle(); - bundle.putParcelable(AccountManager.KEY_INTENT, intent); - - return bundle; - } - - @Override - public Bundle getAccountRemovalAllowed(AccountAuthenticatorResponse response, - Account account) throws NetworkErrorException { - Bundle result = super.getAccountRemovalAllowed(response, account); - - if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) - && !result.containsKey(AccountManager.KEY_INTENT)) { - boolean allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT); - - if (allowed) { - for (String auth : SYNC_AUTHORITIES) { - ContentResolver.cancelSync(account, auth); - } - } - } - - return result; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt new file mode 100644 index 0000000000..367989f142 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticator.kt @@ -0,0 +1,108 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AbstractAccountAuthenticator +import android.accounts.Account +import android.accounts.AccountAuthenticatorResponse +import android.accounts.AccountManager +import android.accounts.NetworkErrorException +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.core.os.bundleOf +import fr.free.nrw.commons.BuildConfig + +private val SYNC_AUTHORITIES = arrayOf( + BuildConfig.CONTRIBUTION_AUTHORITY, BuildConfig.MODIFICATION_AUTHORITY +) + +/** + * Handles WikiMedia commons account Authentication + */ +class WikiAccountAuthenticator( + private val context: Context +) : AbstractAccountAuthenticator(context) { + /** + * Provides Bundle with edited Account Properties + */ + override fun editProperties( + response: AccountAuthenticatorResponse, + accountType: String + ) = bundleOf("test" to "editProperties") + + // account type not supported returns bundle without loginActivity Intent, it just contains "test" key + @Throws(NetworkErrorException::class) + override fun addAccount( + response: AccountAuthenticatorResponse, + accountType: String, + authTokenType: String?, + requiredFeatures: Array?, + options: Bundle? + ) = if (BuildConfig.ACCOUNT_TYPE == accountType) { + addAccount(response) + } else { + bundleOf("test" to "addAccount") + } + + @Throws(NetworkErrorException::class) + override fun confirmCredentials( + response: AccountAuthenticatorResponse, account: Account, options: Bundle? + ) = bundleOf("test" to "confirmCredentials") + + @Throws(NetworkErrorException::class) + override fun getAuthToken( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String, + options: Bundle? + ) = bundleOf("test" to "getAuthToken") + + override fun getAuthTokenLabel(authTokenType: String) = + if (BuildConfig.ACCOUNT_TYPE == authTokenType) AUTH_TOKEN_TYPE else null + + @Throws(NetworkErrorException::class) + override fun updateCredentials( + response: AccountAuthenticatorResponse, + account: Account, + authTokenType: String?, + options: Bundle? + ) = bundleOf("test" to "updateCredentials") + + @Throws(NetworkErrorException::class) + override fun hasFeatures( + response: AccountAuthenticatorResponse, + account: Account, features: Array + ) = bundleOf(AccountManager.KEY_BOOLEAN_RESULT to false) + + /** + * Provides a bundle containing a Parcel + * the Parcel packs an Intent with LoginActivity and Authenticator response (requires valid account type) + */ + private fun addAccount(response: AccountAuthenticatorResponse): Bundle { + val intent = Intent(context, LoginActivity::class.java) + .putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response) + return bundleOf(AccountManager.KEY_INTENT to intent) + } + + @Throws(NetworkErrorException::class) + override fun getAccountRemovalAllowed( + response: AccountAuthenticatorResponse?, + account: Account? + ): Bundle { + val result = super.getAccountRemovalAllowed(response, account) + + if (result.containsKey(AccountManager.KEY_BOOLEAN_RESULT) + && !result.containsKey(AccountManager.KEY_INTENT) + ) { + val allowed = result.getBoolean(AccountManager.KEY_BOOLEAN_RESULT) + + if (allowed) { + for (auth in SYNC_AUTHORITIES) { + ContentResolver.cancelSync(account, auth) + } + } + } + + return result + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java deleted file mode 100644 index bb41f27aa2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.auth; - -import android.accounts.AbstractAccountAuthenticator; -import android.content.Intent; -import android.os.IBinder; - -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.di.CommonsDaggerService; - -/** - * Handles the Auth service of the App, see AndroidManifests for details - * (Uses Dagger 2 as injector) - */ -public class WikiAccountAuthenticatorService extends CommonsDaggerService { - - @Nullable - private AbstractAccountAuthenticator authenticator; - - @Override - public void onCreate() { - super.onCreate(); - authenticator = new WikiAccountAuthenticator(this); - } - - @Nullable - @Override - public IBinder onBind(Intent intent) { - return authenticator == null ? null : authenticator.getIBinder(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt new file mode 100644 index 0000000000..852536a489 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/auth/WikiAccountAuthenticatorService.kt @@ -0,0 +1,22 @@ +package fr.free.nrw.commons.auth + +import android.accounts.AbstractAccountAuthenticator +import android.content.Intent +import android.os.IBinder +import fr.free.nrw.commons.di.CommonsDaggerService + +/** + * Handles the Auth service of the App, see AndroidManifests for details + * (Uses Dagger 2 as injector) + */ +class WikiAccountAuthenticatorService : CommonsDaggerService() { + private var authenticator: AbstractAccountAuthenticator? = null + + override fun onCreate() { + super.onCreate() + authenticator = WikiAccountAuthenticator(this) + } + + override fun onBind(intent: Intent): IBinder? = + authenticator?.iBinder +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java index cd7324c633..3f9344184b 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java @@ -14,7 +14,6 @@ import dagger.Provides; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao; @@ -114,11 +113,6 @@ public Map provideLicensesByName(Context context) { return byName; } - @Provides - public AccountUtil providesAccountUtil(Context context) { - return new AccountUtil(); - } - /** * Provides an instance of CategoryContentProviderClient i.e. the categories * that are there in local storage diff --git a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java index 1723da7238..2a4b612c02 100644 --- a/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java +++ b/app/src/main/java/fr/free/nrw/commons/feedback/FeedbackContentCreator.java @@ -2,7 +2,7 @@ import android.content.Context; import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.AccountUtilKt; import fr.free.nrw.commons.feedback.model.Feedback; import fr.free.nrw.commons.utils.LangCodeUtils; import java.util.Locale; @@ -43,7 +43,7 @@ public void init() { sectionTitleBuilder = new StringBuilder(); sectionTitleBuilder.append("Feedback from "); - sectionTitleBuilder.append(AccountUtil.getUserName(context)); + sectionTitleBuilder.append(AccountUtilKt.getUserName(context)); sectionTitleBuilder.append(" for version "); sectionTitleBuilder.append(feedback.getVersion()); sectionTitleBuilder.append(" on "); diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index ed20809acd..66f2221b82 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -53,7 +53,7 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.actions.ThanksClient; -import fr.free.nrw.commons.auth.AccountUtil; +import fr.free.nrw.commons.auth.AccountUtilKt; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException; import fr.free.nrw.commons.category.CategoryClient; @@ -382,8 +382,8 @@ public void onResume() { enableProgressBar(); } - if (AccountUtil.getUserName(getContext()) != null && media != null - && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) != null && media != null + && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { binding.sendThanks.setVisibility(GONE); } else { binding.sendThanks.setVisibility(VISIBLE); @@ -485,7 +485,7 @@ private void onDiscussionLoaded(String discussion) { } private void onDeletionPageExists(Boolean deletionPageExists) { - if (AccountUtil.getUserName(getContext()) == null && !AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) == null && !AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { binding.nominateDeletion.setVisibility(GONE); binding.nominatedDeletionBanner.setVisibility(GONE); } else if (deletionPageExists) { @@ -1079,7 +1079,7 @@ private void updateCaptions(UploadMediaDetail mediaDetail, @SuppressLint("StringFormatInvalid") public void onDeleteButtonClicked(){ - if (AccountUtil.getUserName(getContext()) != null && AccountUtil.getUserName(getContext()).equals(media.getAuthor())) { + if (AccountUtilKt.getUserName(getContext()) != null && AccountUtilKt.getUserName(getContext()).equals(media.getAuthor())) { final ArrayAdapter languageAdapter = new ArrayAdapter<>(getActivity(), R.layout.simple_spinner_dropdown_list, reasonList); final Spinner spinner = new Spinner(getActivity()); @@ -1105,7 +1105,7 @@ public void onDeleteButtonClicked(){ //Reviewer correct me if i have misunderstood something over here //But how does this if (delete.getVisibility() == View.VISIBLE) { // enableDeleteButton(true); makes sense ? - else if (AccountUtil.getUserName(getContext()) != null) { + else if (AccountUtilKt.getUserName(getContext()) != null) { final EditText input = new EditText(getActivity()); input.requestFocus(); AlertDialog d = DialogUtil.showAlertDialog(getActivity(), diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt index 1d87a8f82c..1547f89ad0 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.kt @@ -96,7 +96,7 @@ class NotificationActivity : BaseActivity() { } }, { throwable -> if (throwable is InvalidLoginTokenException) { - val username = sessionManager.getUserName() + val username = sessionManager.userName val logoutListener = CommonsApplication.BaseLogoutListener( this, getString(R.string.invalid_login_message), diff --git a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt index 44b0f9bc1b..cd2cbc8ad3 100644 --- a/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.kt @@ -11,7 +11,7 @@ import android.view.MotionEvent import android.view.View import fr.free.nrw.commons.Media import fr.free.nrw.commons.R -import fr.free.nrw.commons.auth.AccountUtil +import fr.free.nrw.commons.auth.getUserName import fr.free.nrw.commons.databinding.ActivityReviewBinding import fr.free.nrw.commons.delete.DeleteHelper import fr.free.nrw.commons.media.MediaDetailFragment @@ -183,7 +183,7 @@ class ReviewActivity : BaseActivity() { } //If The Media User and Current Session Username is same then Skip the Image - if (media.user == AccountUtil.getUserName(applicationContext)) { + if (media.user == getUserName(applicationContext)) { runRandomizer() return } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt index c0e5097c06..dbbab73596 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsFragment.kt @@ -63,7 +63,7 @@ class FailedUploadsFragment : } if (StringUtils.isEmpty(userName)) { - userName = sessionManager.getUserName() + userName = sessionManager.userName } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt index dd06452f91..7e7275049d 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/AbstractTextWatcher.kt @@ -19,7 +19,7 @@ class AbstractTextWatcher( // No-op } - interface TextChange { + fun interface TextChange { fun onTextChanged(value: String) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 84ec5a2cba..4c38a30ff1 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -6,7 +6,6 @@ import android.content.Context import androidx.collection.LruCache import com.google.gson.Gson import com.nhaarman.mockitokotlin2.mock -import fr.free.nrw.commons.auth.AccountUtil import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsApplicationComponent import fr.free.nrw.commons.di.CommonsApplicationModule @@ -41,7 +40,6 @@ class TestCommonsApplication : Application() { class MockCommonsApplicationModule( appContext: Context, ) : CommonsApplicationModule(appContext) { - val accountUtil: AccountUtil = mock() val defaultSharedPreferences: JsonKvStore = mock() val locationServiceManager: LocationServiceManager = mock() val mockDbOpenHelper: DBOpenHelper = mock() @@ -58,8 +56,6 @@ class MockCommonsApplicationModule( override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient - override fun providesAccountUtil(context: Context): AccountUtil = accountUtil - override fun providesDefaultKvStore( context: Context, gson: Gson, diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt index b45b844c06..566484a029 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/AccountUtilUnitTest.kt @@ -15,25 +15,17 @@ import org.robolectric.annotation.Config @Config(sdk = [21], application = TestCommonsApplication::class) class AccountUtilUnitTest { private lateinit var context: FakeContextWrapper - private lateinit var accountUtil: AccountUtil @Before @Throws(Exception::class) fun setUp() { context = FakeContextWrapper(ApplicationProvider.getApplicationContext()) - accountUtil = AccountUtil() - } - - @Test - @Throws(Exception::class) - fun checkNotNull() { - Assert.assertNotNull(accountUtil) } @Test @Throws(Exception::class) fun testGetUserName() { - Assert.assertEquals(AccountUtil.getUserName(context), "test@example.com") + Assert.assertEquals(getUserName(context), "test@example.com") } @Test @@ -41,13 +33,13 @@ class AccountUtilUnitTest { fun testGetUserNameWithException() { val context = FakeContextWrapperWithException(ApplicationProvider.getApplicationContext()) - Assert.assertEquals(AccountUtil.getUserName(context), null) + Assert.assertEquals(getUserName(context), null) } @Test @Throws(Exception::class) fun testAccount() { - Assert.assertEquals(AccountUtil.account(context)?.name, "test@example.com") + Assert.assertEquals(account(context)?.name, "test@example.com") } @Test @@ -55,6 +47,6 @@ class AccountUtilUnitTest { fun testAccountWithException() { val context = FakeContextWrapperWithException(ApplicationProvider.getApplicationContext()) - Assert.assertEquals(AccountUtil.account(context), null) + Assert.assertEquals(account(context), null) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt index 162f505848..871613ca51 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/LoginActivityUnitTests.kt @@ -218,17 +218,6 @@ class LoginActivityUnitTests { method.invoke(activity) } - @Test - @Throws(Exception::class) - fun testHideProgress() { - val method: Method = - LoginActivity::class.java.getDeclaredMethod( - "hideProgress", - ) - method.isAccessible = true - method.invoke(activity) - } - @Test @Throws(Exception::class) fun testOnResume() { diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt index c1c2094623..370c74a47d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorServiceUnitTest.kt @@ -3,18 +3,13 @@ package fr.free.nrw.commons.auth import org.junit.Assert import org.junit.Before import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import java.lang.reflect.Field class WikiAccountAuthenticatorServiceUnitTest { - private lateinit var service: WikiAccountAuthenticatorService - - @Before - fun setUp() { - MockitoAnnotations.openMocks(this) - service = WikiAccountAuthenticatorService() - service.onBind(null) - } + private val service = WikiAccountAuthenticatorService() @Test fun checkNotNull() { @@ -23,10 +18,9 @@ class WikiAccountAuthenticatorServiceUnitTest { @Test fun testOnBindCaseNull() { - val field: Field = - WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator") + val field: Field = WikiAccountAuthenticatorService::class.java.getDeclaredField("authenticator") field.isAccessible = true field.set(service, null) - Assert.assertEquals(service.onBind(null), null) + Assert.assertEquals(service.onBind(mock()), null) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt index 856e5015d0..d5e24790b8 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/auth/WikiAccountAuthenticatorUnitTest.kt @@ -86,10 +86,7 @@ class WikiAccountAuthenticatorUnitTest { @Test fun testGetAuthTokenLabelCaseNonNull() { - Assert.assertEquals( - authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), - AccountUtil.AUTH_TOKEN_TYPE, - ) + Assert.assertEquals(authenticator.getAuthTokenLabel(BuildConfig.ACCOUNT_TYPE), AUTH_TOKEN_TYPE) } @Test From 794dbb8f9273654ff0bf70ba83288b7ead7d2fd4 Mon Sep 17 00:00:00 2001 From: Rohit Verma <101377978+rohit9625@users.noreply.github.com> Date: Thu, 28 Nov 2024 14:07:56 +0530 Subject: [PATCH 038/231] Fix modification on bottom sheet's data when coming from Nearby Banner and clicked on other pins (#5937) * refactor getIconFor method and remove call to highlightNearestPlace() It ensures single responsibility on getIconFor method * refactor and fix icon issue when coming from nearby banner --------- Co-authored-by: Nicolas Raoul --- .../fragments/NearbyParentFragment.java | 43 +++++++++++-------- .../main/res/layout/bottom_sheet_details.xml | 2 +- 2 files changed, 26 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index fdbc727bc6..5da4e5478f 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java @@ -965,6 +965,7 @@ public void centerMapToPlace(@Nullable final Place place) { lastPlaceToCenter.location.getLatitude() - cameraShift, lastPlaceToCenter.getLocation().getLongitude(), 0)); } + highlightNearestPlace(place); } @@ -2001,7 +2002,8 @@ public void updateMarker(final boolean isBookmarked, final Place place, * * @param nearestPlace nearest place, which has to be highlighted */ - private void highlightNearestPlace(Place nearestPlace) { + private void highlightNearestPlace(final Place nearestPlace) { + binding.bottomSheetDetails.icon.setVisibility(View.VISIBLE); passInfoToSheet(nearestPlace); hideBottomSheet(); bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED); @@ -2015,32 +2017,37 @@ private void highlightNearestPlace(Place nearestPlace) { * @return returns the drawable of marker according to the place information */ private @DrawableRes int getIconFor(Place place, Boolean isBookmarked) { - if (nearestPlace != null) { - if (place.name.equals(nearestPlace.name)) { - // Highlight nearest place only when user clicks on the home nearby banner - highlightNearestPlace(place); - return (isBookmarked ? - R.drawable.ic_custom_map_marker_purple_bookmarked : - R.drawable.ic_custom_map_marker_purple); - } + if (nearestPlace != null && place.name.equals(nearestPlace.name)) { + // Highlight nearest place only when user clicks on the home nearby banner +// highlightNearestPlace(place); + return (isBookmarked ? + R.drawable.ic_custom_map_marker_purple_bookmarked : + R.drawable.ic_custom_map_marker_purple + ); } + if (place.isMonument()) { return R.drawable.ic_custom_map_marker_monuments; - } else if (!place.pic.trim().isEmpty()) { + } + if (!place.pic.trim().isEmpty()) { return (isBookmarked ? R.drawable.ic_custom_map_marker_green_bookmarked : - R.drawable.ic_custom_map_marker_green); - } else if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed + R.drawable.ic_custom_map_marker_green + ); + } + if (!place.exists) { // Means that the topic of the Wikidata item does not exist in the real world anymore, for instance it is a past event, or a place that was destroyed return (R.drawable.ic_clear_black_24dp); - }else if (place.name == "") { + } + if (place.name.isEmpty()) { return (isBookmarked ? R.drawable.ic_custom_map_marker_grey_bookmarked : - R.drawable.ic_custom_map_marker_grey); - } else { - return (isBookmarked ? - R.drawable.ic_custom_map_marker_red_bookmarked : - R.drawable.ic_custom_map_marker_red); + R.drawable.ic_custom_map_marker_grey + ); } + return (isBookmarked ? + R.drawable.ic_custom_map_marker_red_bookmarked : + R.drawable.ic_custom_map_marker_red + ); } /** diff --git a/app/src/main/res/layout/bottom_sheet_details.xml b/app/src/main/res/layout/bottom_sheet_details.xml index f026528b65..77d31a967a 100644 --- a/app/src/main/res/layout/bottom_sheet_details.xml +++ b/app/src/main/res/layout/bottom_sheet_details.xml @@ -32,7 +32,7 @@ android:layout_width="@dimen/dimen_40" android:layout_height="@dimen/dimen_40" android:layout_marginLeft="@dimen/standard_gap" - android:visibility="gone"> + android:visibility="gone" /> Date: Thu, 28 Nov 2024 13:01:55 +0100 Subject: [PATCH 039/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-es/strings.xml | 21 +++++++++++++++++++++ app/src/main/res/values-iw/strings.xml | 6 +++--- app/src/main/res/values-qq/strings.xml | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 4e90f68641..f02ea7dacb 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -29,6 +29,7 @@ * Jduranboger * Jelou * Johnny243 +* Josuert * Juanman * Keneth Urrutia * Ktranz @@ -166,6 +167,7 @@ Buscar categorías Buscar elementos que tu archivo multimedia representa (montaña, Taj Mahal, etc.) Guardar + Menú de desbordamiento Actualizar Lista (No hay subidas aún) @@ -522,6 +524,7 @@ No tienes notificaciones sin leer No tienes ninguna notificación leída Compartir registros usando + Revisa tu bandeja de entrada Ver leídas Ver no leidas Ocurrió un error mientras se elegían imagenes @@ -819,8 +822,26 @@ Por favor, escriba algunos comentarios. Discusión Escriba algo sobre el elemento \'%1$s\'. Será visible públicamente. + \' %1$s \' ya no existe, nunca se podrá tomar ninguna fotografía de él. + \' %1$s \' está en un lugar diferente. Especifique el lugar correcto a continuación y, si es posible, escriba la latitud y longitud correctas. + Otro problema o información (por favor explique a continuación). + Sus comentarios se publicarán en la siguiente página wiki: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile_app/Feedback</a> + ¿Estás seguro de que deseas cancelar todas las subidas? Cancelando todas las subidas... Subidas Pendiente Falló + No se pudieron cargar los datos del lugar + Eliminar carpeta + Confirmar eliminación + ¿Está seguro de que deseas eliminar la carpeta %1$s que contiene %2$d elementos? + Eliminar + Cancelar + La carpeta %1$s se eliminó correctamente + No se pudo eliminar la carpeta %1$s + Error al eliminar el contenido de la carpeta: %1$s + No se pudo recuperar la ruta de la carpeta para el ID del bucket: %1$d + Este lugar aún no tiene foto, ¡ve y toma una! + Este lugar ya tiene una foto. + Ahora comprobando si este lugar tiene una foto. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index be45099a90..a77bdfea93 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -542,9 +542,9 @@ %1$s הועלה על ידי: %2$s שפת התיאור כבררת מחדל העמדה למחיקה - הצלחה + זה עבד הקובץ %1$s הועמד למחיקה. - כשלון + זה לא עבד לא ניתן לבקש מחיקה תמונה עצמית (סלפי) שלא משמשת בשום ערך תמונה מטושטשת לגמרי @@ -825,7 +825,7 @@ מחיקת תיקייה אישור מחיקה למחוק את התיקייה %1$s על כל %2$d פריטיה? - מחיקה + למחוק ביטול התיקייה %1$s נמחקה מחיקת התיקייה %1$s נכשלה diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index e06969dc23..833743aefc 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -204,4 +204,5 @@ {{Identical|Detail}} \"Set as avatar\" should be translated the same as {{msg-wm|Commons-android-strings-menu set avatar}}. {{Doc-commons-app-depicts}} + An answer to the question in {{msg-wm|Commons-android-strings-custom selector confirm deletion message}}. From 1afff73c24a7c5bb7486a3846620cff337b98906 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 22:44:26 -0600 Subject: [PATCH 040/231] Migrate campaigns package to kotlin (#5969) * Convert ICampaignsView to kotlin along with simple fix * Convert CampaignView to Kotlin * Convert CampaignsPresenter to Kotlin * Convert CampaignsPresenter to kotlin --------- Co-authored-by: Nicolas Raoul --- .../nrw/commons/campaigns/CampaignView.java | 118 ----------------- .../nrw/commons/campaigns/CampaignView.kt | 121 +++++++++++++++++ .../commons/campaigns/CampaignsPresenter.java | 123 ------------------ .../commons/campaigns/CampaignsPresenter.kt | 107 +++++++++++++++ .../nrw/commons/campaigns/ICampaignsView.java | 11 -- .../nrw/commons/campaigns/ICampaignsView.kt | 11 ++ .../campaigns/CampaignsPresenterTest.kt | 17 ++- 7 files changed, 247 insertions(+), 261 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java create mode 100644 app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java deleted file mode 100644 index d1ee4c8b06..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java +++ /dev/null @@ -1,118 +0,0 @@ -package fr.free.nrw.commons.campaigns; - -import android.content.Context; -import android.net.Uri; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.campaigns.models.Campaign; -import fr.free.nrw.commons.databinding.LayoutCampaginBinding; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.CommonsDateUtil; - -import fr.free.nrw.commons.utils.DateUtil; -import java.text.ParseException; -import java.util.Date; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.utils.SwipableCardView; -import fr.free.nrw.commons.utils.ViewUtil; - -/** - * A view which represents a single campaign - */ -public class CampaignView extends SwipableCardView { - Campaign campaign; - private LayoutCampaginBinding binding; - private ViewHolder viewHolder; - - public static final String CAMPAIGNS_DEFAULT_PREFERENCE = "displayCampaignsCardView"; - public static final String WLM_CARD_PREFERENCE = "displayWLMCardView"; - - private String campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE; - - public CampaignView(@NonNull Context context) { - super(context); - init(); - } - - public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - init(); - } - - public CampaignView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - init(); - } - - public void setCampaign(final Campaign campaign) { - this.campaign = campaign; - if (campaign != null) { - if (campaign.isWLMCampaign()) { - campaignPreference = WLM_CARD_PREFERENCE; - } - setVisibility(View.VISIBLE); - viewHolder.init(); - } else { - this.setVisibility(View.GONE); - } - } - - @Override public boolean onSwipe(final View view) { - view.setVisibility(View.GONE); - ((BaseActivity) getContext()).defaultKvStore - .putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false); - ViewUtil.showLongToast(getContext(), - getResources().getString(R.string.nearby_campaign_dismiss_message)); - return true; - } - - private void init() { - binding = LayoutCampaginBinding.inflate(LayoutInflater.from(getContext()), this, true); - viewHolder = new ViewHolder(); - setOnClickListener(view -> { - if (campaign != null) { - if (campaign.isWLMCampaign()) { - ((MainActivity)(getContext())).showNearby(); - } else { - Utils.handleWebUrl(getContext(), Uri.parse(campaign.getLink())); - } - } - }); - } - - public class ViewHolder { - public void init() { - if (campaign != null) { - binding.ivCampaign.setImageDrawable( - getResources().getDrawable(R.drawable.ic_campaign)); - - binding.tvTitle.setText(campaign.getTitle()); - binding.tvDescription.setText(campaign.getDescription()); - try { - if (campaign.isWLMCampaign()) { - binding.tvDates.setText( - String.format("%1s - %2s", campaign.getStartDate(), - campaign.getEndDate())); - } else { - final Date startDate = CommonsDateUtil.getIso8601DateFormatShort() - .parse(campaign.getStartDate()); - final Date endDate = CommonsDateUtil.getIso8601DateFormatShort() - .parse(campaign.getEndDate()); - binding.tvDates.setText(String.format("%1s - %2s", DateUtil.getExtraShortDateString(startDate), - DateUtil.getExtraShortDateString(endDate))); - } - } catch (final ParseException e) { - e.printStackTrace(); - } - } - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt new file mode 100644 index 0000000000..7a47201776 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.campaigns + +import android.content.Context +import android.net.Uri +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.core.content.ContextCompat +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.campaigns.models.Campaign +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.databinding.LayoutCampaginBinding +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort +import fr.free.nrw.commons.utils.DateUtil.getExtraShortDateString +import fr.free.nrw.commons.utils.SwipableCardView +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import timber.log.Timber +import java.text.ParseException + +/** + * A view which represents a single campaign + */ +class CampaignView : SwipableCardView { + private var campaign: Campaign? = null + private var binding: LayoutCampaginBinding? = null + private var viewHolder: ViewHolder? = null + private var campaignPreference = CAMPAIGNS_DEFAULT_PREFERENCE + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( + context, attrs, defStyleAttr) { + init() + } + + fun setCampaign(campaign: Campaign?) { + this.campaign = campaign + if (campaign != null) { + if (campaign.isWLMCampaign) { + campaignPreference = WLM_CARD_PREFERENCE + } + visibility = VISIBLE + viewHolder!!.init() + } else { + visibility = GONE + } + } + + override fun onSwipe(view: View): Boolean { + view.visibility = GONE + (context as BaseActivity).defaultKvStore.putBoolean(CAMPAIGNS_DEFAULT_PREFERENCE, false) + showLongToast( + context, + resources.getString(R.string.nearby_campaign_dismiss_message) + ) + return true + } + + private fun init() { + binding = LayoutCampaginBinding.inflate( + LayoutInflater.from(context), this, true + ) + viewHolder = ViewHolder() + setOnClickListener { + campaign?.let { + if (it.isWLMCampaign) { + ((context) as MainActivity).showNearby() + } else { + Utils.handleWebUrl(context, Uri.parse(it.link)) + } + } + } + } + + inner class ViewHolder { + fun init() { + if (campaign != null) { + binding!!.ivCampaign.setImageDrawable( + ContextCompat.getDrawable(binding!!.root.context, R.drawable.ic_campaign) + ) + binding!!.tvTitle.text = campaign!!.title + binding!!.tvDescription.text = campaign!!.description + try { + if (campaign!!.isWLMCampaign) { + binding!!.tvDates.text = String.format( + "%1s - %2s", campaign!!.startDate, + campaign!!.endDate + ) + } else { + val startDate = getIso8601DateFormatShort().parse( + campaign?.startDate + ) + val endDate = getIso8601DateFormatShort().parse( + campaign?.endDate + ) + binding!!.tvDates.text = String.format( + "%1s - %2s", getExtraShortDateString( + startDate!! + ), getExtraShortDateString(endDate!!) + ) + } + } catch (e: ParseException) { + Timber.e(e) + } + } + } + } + + companion object { + const val CAMPAIGNS_DEFAULT_PREFERENCE: String = "displayCampaignsCardView" + const val WLM_CARD_PREFERENCE: String = "displayWLMCardView" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java deleted file mode 100644 index 157047774e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.java +++ /dev/null @@ -1,123 +0,0 @@ -package fr.free.nrw.commons.campaigns; - -import android.annotation.SuppressLint; - -import fr.free.nrw.commons.campaigns.models.Campaign; -import fr.free.nrw.commons.utils.CommonsDateUtil; -import java.text.ParseException; -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; - -import fr.free.nrw.commons.BasePresenter; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.Scheduler; -import io.reactivex.Single; -import io.reactivex.SingleObserver; -import io.reactivex.disposables.Disposable; -import timber.log.Timber; - -import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; -import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD; - -/** - * The presenter for the campaigns view, fetches the campaigns from the api and informs the view on - * success and error - */ -@Singleton -public class CampaignsPresenter implements BasePresenter { - private final OkHttpJsonApiClient okHttpJsonApiClient; - private final Scheduler mainThreadScheduler; - private final Scheduler ioScheduler; - - private ICampaignsView view; - private Disposable disposable; - private Campaign campaign; - - @Inject - public CampaignsPresenter(OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD)Scheduler ioScheduler, @Named(MAIN_THREAD)Scheduler mainThreadScheduler) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.mainThreadScheduler=mainThreadScheduler; - this.ioScheduler=ioScheduler; - } - - @Override - public void onAttachView(ICampaignsView view) { - this.view = view; - } - - @Override public void onDetachView() { - this.view = null; - if (disposable != null) { - disposable.dispose(); - } - } - - /** - * make the api call to fetch the campaigns - */ - @SuppressLint("CheckResult") - public void getCampaigns() { - if (view != null && okHttpJsonApiClient != null) { - //If we already have a campaign, lets not make another call - if (this.campaign != null) { - view.showCampaigns(campaign); - return; - } - Single campaigns = okHttpJsonApiClient.getCampaigns(); - campaigns.observeOn(mainThreadScheduler) - .subscribeOn(ioScheduler) - .subscribeWith(new SingleObserver() { - - @Override public void onSubscribe(Disposable d) { - disposable = d; - } - - @Override public void onSuccess(CampaignResponseDTO campaignResponseDTO) { - List campaigns = campaignResponseDTO.getCampaigns(); - if (campaigns == null || campaigns.isEmpty()) { - Timber.e("The campaigns list is empty"); - view.showCampaigns(null); - return; - } - Collections.sort(campaigns, (campaign, t1) -> { - Date date1, date2; - try { - - date1 = CommonsDateUtil.getIso8601DateFormatShort().parse(campaign.getStartDate()); - date2 = CommonsDateUtil.getIso8601DateFormatShort().parse(t1.getStartDate()); - } catch (ParseException e) { - e.printStackTrace(); - return -1; - } - return date1.compareTo(date2); - }); - Date campaignEndDate, campaignStartDate; - Date currentDate = new Date(); - try { - for (Campaign aCampaign : campaigns) { - campaignEndDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getEndDate()); - campaignStartDate = CommonsDateUtil.getIso8601DateFormatShort().parse(aCampaign.getStartDate()); - if (campaignEndDate.compareTo(currentDate) >= 0 - && campaignStartDate.compareTo(currentDate) <= 0) { - campaign = aCampaign; - break; - } - } - } catch (ParseException e) { - e.printStackTrace(); - } - view.showCampaigns(campaign); - } - - @Override public void onError(Throwable e) { - Timber.e(e, "could not fetch campaigns"); - } - }); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt new file mode 100644 index 0000000000..3753dfb670 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt @@ -0,0 +1,107 @@ +package fr.free.nrw.commons.campaigns + +import android.annotation.SuppressLint +import fr.free.nrw.commons.BasePresenter +import fr.free.nrw.commons.campaigns.models.Campaign +import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort +import io.reactivex.Scheduler +import io.reactivex.disposables.Disposable +import timber.log.Timber +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Date +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +/** + * The presenter for the campaigns view, fetches the campaigns from the api and informs the view on + * success and error + */ +@Singleton +class CampaignsPresenter @Inject constructor( + private val okHttpJsonApiClient: OkHttpJsonApiClient?, + @param:Named(IO_THREAD) private val ioScheduler: Scheduler, + @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler +) : BasePresenter { + private var view: ICampaignsView? = null + private var disposable: Disposable? = null + private var campaign: Campaign? = null + + override fun onAttachView(view: ICampaignsView) { + this.view = view + } + + override fun onDetachView() { + view = null + disposable?.dispose() + } + + /** + * make the api call to fetch the campaigns + */ + @SuppressLint("CheckResult") + fun getCampaigns() { + if (view != null && okHttpJsonApiClient != null) { + //If we already have a campaign, lets not make another call + if (campaign != null) { + view!!.showCampaigns(campaign) + return + } + + okHttpJsonApiClient.campaigns + .observeOn(mainThreadScheduler) + .subscribeOn(ioScheduler) + .doOnSubscribe { disposable = it } + .subscribe({ campaignResponseDTO -> + val campaigns = campaignResponseDTO.campaigns?.toMutableList() + if (campaigns.isNullOrEmpty()) { + Timber.e("The campaigns list is empty") + view!!.showCampaigns(null) + } else { + sortCampaignsByStartDate(campaigns) + campaign = findActiveCampaign(campaigns) + view!!.showCampaigns(campaign) + } + }, { + Timber.e(it, "could not fetch campaigns") + }) + } + } + + private fun sortCampaignsByStartDate(campaigns: MutableList) { + val dateFormat: SimpleDateFormat = getIso8601DateFormatShort() + campaigns.sortWith(Comparator { campaign: Campaign, other: Campaign -> + val date1: Date? + val date2: Date? + try { + date1 = campaign.startDate?.let { dateFormat.parse(it) } + date2 = other.startDate?.let { dateFormat.parse(it) } + } catch (e: ParseException) { + Timber.e(e) + return@Comparator -1 + } + if (date1 != null && date2 != null) date1.compareTo(date2) else -1 + }) + } + + private fun findActiveCampaign(campaigns: List) : Campaign? { + val dateFormat: SimpleDateFormat = getIso8601DateFormatShort() + val currentDate = Date() + return try { + campaigns.firstOrNull { + val campaignStartDate = it.startDate?.let { s -> dateFormat.parse(s) } + val campaignEndDate = it.endDate?.let { s -> dateFormat.parse(s) } + campaignStartDate != null && campaignEndDate != null && + campaignEndDate >= currentDate && campaignStartDate <= currentDate + } + } catch (e: ParseException) { + Timber.e(e, "could not find active campaign") + null + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java deleted file mode 100644 index a1e79cca61..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.free.nrw.commons.campaigns; - -import fr.free.nrw.commons.MvpView; -import fr.free.nrw.commons.campaigns.models.Campaign; - -/** - * Interface which defines the view contracts of the campaign view - */ -public interface ICampaignsView extends MvpView { - void showCampaigns(Campaign campaign); -} diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt new file mode 100644 index 0000000000..62a19aaac9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/ICampaignsView.kt @@ -0,0 +1,11 @@ +package fr.free.nrw.commons.campaigns + +import fr.free.nrw.commons.MvpView +import fr.free.nrw.commons.campaigns.models.Campaign + +/** + * Interface which defines the view contracts of the campaign view + */ +interface ICampaignsView : MvpView { + fun showCampaigns(campaign: Campaign?) +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt index 7efdfd1ad4..ec3ad82f1b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt @@ -20,25 +20,24 @@ import kotlin.collections.ArrayList class CampaignsPresenterTest { @Mock - lateinit var okHttpJsonApiClient: OkHttpJsonApiClient - - lateinit var campaignsPresenter: CampaignsPresenter + private lateinit var okHttpJsonApiClient: OkHttpJsonApiClient @Mock - internal lateinit var view: ICampaignsView + private lateinit var view: ICampaignsView @Mock - internal lateinit var campaignResponseDTO: CampaignResponseDTO - lateinit var campaignsSingle: Single + private lateinit var campaignResponseDTO: CampaignResponseDTO @Mock - lateinit var campaign: Campaign - - lateinit var testScheduler: TestScheduler + private lateinit var campaign: Campaign @Mock private lateinit var disposable: Disposable + private lateinit var campaignsPresenter: CampaignsPresenter + private lateinit var campaignsSingle: Single + private lateinit var testScheduler: TestScheduler + /** * initial setup, test environment */ From d6c4cab207fd93104faef67f85c2f84f781e275f Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 29 Nov 2024 10:20:33 +0530 Subject: [PATCH 041/231] Migrated logging module from Java to Kotlin (#5972) * Migrated logging module from Java to Kotlin * Rename .java to .kt --------- Co-authored-by: Nicolas Raoul --- .../nrw/commons/logging/CommonsLogSender.java | 105 --------- .../nrw/commons/logging/CommonsLogSender.kt | 107 ++++++++++ .../nrw/commons/logging/FileLoggingTree.java | 145 ------------- .../nrw/commons/logging/FileLoggingTree.kt | 133 ++++++++++++ .../commons/logging/LogLevelSettableTree.java | 8 - .../commons/logging/LogLevelSettableTree.kt | 8 + .../fr/free/nrw/commons/logging/LogUtils.java | 48 ----- .../fr/free/nrw/commons/logging/LogUtils.kt | 57 +++++ .../free/nrw/commons/logging/LogsSender.java | 201 ------------------ .../fr/free/nrw/commons/logging/LogsSender.kt | 193 +++++++++++++++++ .../nrw/commons/settings/SettingsFragment.kt | 3 +- 11 files changed, 500 insertions(+), 508 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java create mode 100644 app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java deleted file mode 100644 index 29c2c732ef..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.content.Context; - -import android.os.Bundle; -import javax.inject.Inject; -import javax.inject.Singleton; - -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.DeviceInfoUtil; -import org.acra.data.CrashReportData; -import org.acra.sender.ReportSenderException; -import org.jetbrains.annotations.NotNull; - -/** - * Class responsible for sending logs to developers - */ -@Singleton -public class CommonsLogSender extends LogsSender { - private static final String LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com"; - private static final String LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs"; - private static final String BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs"; - - private SessionManager sessionManager; - private Context context; - - @Inject - public CommonsLogSender(SessionManager sessionManager, - Context context) { - super(sessionManager); - - this.sessionManager = sessionManager; - this.context = context; - boolean isBeta = ConfigUtils.isBetaFlavour(); - this.logFileName = isBeta ? "CommonsBetaAppLogs.zip" : "CommonsAppLogs.zip"; - String emailSubjectFormat = isBeta ? BETA_LOGS_PRIVATE_EMAIL_SUBJECT : LOGS_PRIVATE_EMAIL_SUBJECT; - this.emailSubject = String.format(emailSubjectFormat, sessionManager.getUserName()); - this.emailBody = getExtraInfo(); - this.mailTo = LOGS_PRIVATE_EMAIL; - } - - /** - * Attach any extra meta information about user or device that might help in debugging - * @return String with extra meta information useful for debugging - */ - @Override - public String getExtraInfo() { - StringBuilder builder = new StringBuilder(); - - // Getting API Level - builder.append("API level: ") - .append(DeviceInfoUtil.getAPILevel()) - .append("\n"); - - // Getting Android Version - builder.append("Android version: ") - .append(DeviceInfoUtil.getAndroidVersion()) - .append("\n"); - - // Getting Device Manufacturer - builder.append("Device manufacturer: ") - .append(DeviceInfoUtil.getDeviceManufacturer()) - .append("\n"); - - // Getting Device Model - builder.append("Device model: ") - .append(DeviceInfoUtil.getDeviceModel()) - .append("\n"); - - // Getting Device Name - builder.append("Device: ") - .append(DeviceInfoUtil.getDevice()) - .append("\n"); - - // Getting Network Type - builder.append("Network type: ") - .append(DeviceInfoUtil.getConnectionType(context)) - .append("\n"); - - // Getting App Version - builder.append("App version name: ") - .append(ConfigUtils.getVersionNameWithSha(context)) - .append("\n"); - - // Getting Username - builder.append("User name: ") - .append(sessionManager.getUserName()) - .append("\n"); - - - return builder.toString(); - } - - @Override - public boolean requiresForeground() { - return false; - } - - @Override - public void send(@NotNull Context context, @NotNull CrashReportData crashReportData, - @NotNull Bundle bundle) throws ReportSenderException { - - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt new file mode 100644 index 0000000000..7c6b988a60 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/CommonsLogSender.kt @@ -0,0 +1,107 @@ +package fr.free.nrw.commons.logging + +import android.content.Context + +import android.os.Bundle +import javax.inject.Inject +import javax.inject.Singleton + +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.utils.ConfigUtils +import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha +import fr.free.nrw.commons.utils.DeviceInfoUtil +import org.acra.data.CrashReportData + + +/** + * Class responsible for sending logs to developers + */ +@Singleton +class CommonsLogSender @Inject constructor( + private val sessionManager: SessionManager, + private val context: Context +) : LogsSender(sessionManager) { + + + companion object { + private const val LOGS_PRIVATE_EMAIL = "commons-app-android-private@googlegroups.com" + private const val LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Android App (%s) Logs" + private const val BETA_LOGS_PRIVATE_EMAIL_SUBJECT = "Commons Beta Android App (%s) Logs" + } + + init { + val isBeta = ConfigUtils.isBetaFlavour + logFileName = if (isBeta) "CommonsBetaAppLogs.zip" else "CommonsAppLogs.zip" + val emailSubjectFormat = if (isBeta) + BETA_LOGS_PRIVATE_EMAIL_SUBJECT + else + LOGS_PRIVATE_EMAIL_SUBJECT + emailSubject = emailSubjectFormat.format(sessionManager.userName) + emailBody = getExtraInfo() + mailTo = LOGS_PRIVATE_EMAIL + } + + /** + * Attach any extra meta information about the user or device that might help in debugging. + * @return String with extra meta information useful for debugging. + */ + public override fun getExtraInfo(): String { + return buildString { + // Getting API Level + append("API level: ") + .append(DeviceInfoUtil.getAPILevel()) + .append("\n") + + // Getting Android Version + append("Android version: ") + .append(DeviceInfoUtil.getAndroidVersion()) + .append("\n") + + // Getting Device Manufacturer + append("Device manufacturer: ") + .append(DeviceInfoUtil.getDeviceManufacturer()) + .append("\n") + + // Getting Device Model + append("Device model: ") + .append(DeviceInfoUtil.getDeviceModel()) + .append("\n") + + // Getting Device Name + append("Device: ") + .append(DeviceInfoUtil.getDevice()) + .append("\n") + + // Getting Network Type + append("Network type: ") + .append(DeviceInfoUtil.getConnectionType(context)) + .append("\n") + + // Getting App Version + append("App version name: ") + .append(context.getVersionNameWithSha()) + .append("\n") + + // Getting Username + append("User name: ") + .append(sessionManager.userName) + .append("\n") + } + } + + /** + * Determines if the log sending process requires the app to be in the foreground. + * @return False as it does not require foreground execution. + */ + override fun requiresForeground(): Boolean = false + + /** + * Sends logs to developers. Implementation can be extended. + */ + override fun send( + context: Context, + errorContent: CrashReportData, + extras: Bundle) { + // Add logic here if needed. + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java deleted file mode 100644 index a2ebeec686..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.java +++ /dev/null @@ -1,145 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Locale; -import java.util.concurrent.Executor; - -import ch.qos.logback.classic.LoggerContext; -import ch.qos.logback.classic.encoder.PatternLayoutEncoder; -import ch.qos.logback.classic.spi.ILoggingEvent; -import ch.qos.logback.core.rolling.FixedWindowRollingPolicy; -import ch.qos.logback.core.rolling.RollingFileAppender; -import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy; -import timber.log.Timber; - -/** - * Extends Timber's debug tree to write logs to a file - */ -public class FileLoggingTree extends Timber.DebugTree implements LogLevelSettableTree { - private final Logger logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - private int logLevel; - private final String logFileName; - private int fileSize; - private FixedWindowRollingPolicy rollingPolicy; - private final Executor executor; - - public FileLoggingTree(int logLevel, - String logFileName, - String logDirectory, - int fileSizeInKb, - Executor executor) { - this.logLevel = logLevel; - this.logFileName = logFileName; - this.fileSize = fileSizeInKb; - configureLogger(logDirectory); - this.executor = executor; - } - - /** - * Can be overridden to change file's log level - * @param logLevel - */ - @Override - public void setLogLevel(int logLevel) { - this.logLevel = logLevel; - } - - /** - * Check and log any message - * @param priority - * @param tag - * @param message - * @param t - */ - @Override - protected void log(final int priority, final String tag, @NonNull final String message, Throwable t) { - executor.execute(() -> logMessage(priority, tag, message)); - - } - - /** - * Log any message based on the priority - * @param priority - * @param tag - * @param message - */ - private void logMessage(int priority, String tag, String message) { - String messageWithTag = String.format("[%s] : %s", tag, message); - switch (priority) { - case Log.VERBOSE: - logger.trace(messageWithTag); - break; - case Log.DEBUG: - logger.debug(messageWithTag); - break; - case Log.INFO: - logger.info(messageWithTag); - break; - case Log.WARN: - logger.warn(messageWithTag); - break; - case Log.ERROR: - logger.error(messageWithTag); - break; - case Log.ASSERT: - logger.error(messageWithTag); - break; - } - } - - /** - * Checks if a particular log line should be logged in the file or not - * @param priority - * @return - */ - @Override - protected boolean isLoggable(int priority) { - return priority >= logLevel; - } - - /** - * Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy) - * https://github.com/tony19/logback-android/wiki - * @param logDir - */ - private void configureLogger(String logDir) { - LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); - loggerContext.reset(); - - RollingFileAppender rollingFileAppender = new RollingFileAppender<>(); - rollingFileAppender.setContext(loggerContext); - rollingFileAppender.setFile(logDir + "/" + logFileName + ".0.log"); - - rollingPolicy = new FixedWindowRollingPolicy(); - rollingPolicy.setContext(loggerContext); - rollingPolicy.setMinIndex(1); - rollingPolicy.setMaxIndex(4); - rollingPolicy.setParent(rollingFileAppender); - rollingPolicy.setFileNamePattern(logDir + "/" + logFileName + ".%i.log"); - rollingPolicy.start(); - - SizeBasedTriggeringPolicy triggeringPolicy = new SizeBasedTriggeringPolicy<>(); - triggeringPolicy.setContext(loggerContext); - triggeringPolicy.setMaxFileSize(String.format(Locale.ENGLISH, "%dKB", fileSize)); - triggeringPolicy.start(); - - PatternLayoutEncoder encoder = new PatternLayoutEncoder(); - encoder.setContext(loggerContext); - encoder.setPattern("%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n"); - encoder.start(); - - rollingFileAppender.setEncoder(encoder); - rollingFileAppender.setRollingPolicy(rollingPolicy); - rollingFileAppender.setTriggeringPolicy(triggeringPolicy); - rollingFileAppender.start(); - ch.qos.logback.classic.Logger logger = (ch.qos.logback.classic.Logger) - LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); - logger.addAppender(rollingFileAppender); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt new file mode 100644 index 0000000000..5c6c55f1ac --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/FileLoggingTree.kt @@ -0,0 +1,133 @@ +package fr.free.nrw.commons.logging + +import android.util.Log + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +import java.util.Locale +import java.util.concurrent.Executor + +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.encoder.PatternLayoutEncoder +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.rolling.FixedWindowRollingPolicy +import ch.qos.logback.core.rolling.RollingFileAppender +import ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy +import timber.log.Timber + + +/** + * Extends Timber's debug tree to write logs to a file. + */ +class FileLoggingTree( + private var logLevel: Int, + private val logFileName: String, + logDirectory: String, + private val fileSizeInKb: Int, + private val executor: Executor +) : Timber.DebugTree(), LogLevelSettableTree { + + private val logger: Logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME) + private lateinit var rollingPolicy: FixedWindowRollingPolicy + + init { + configureLogger(logDirectory) + } + + /** + * Can be overridden to change the file's log level. + * @param logLevel The new log level. + */ + override fun setLogLevel(logLevel: Int) { + this.logLevel = logLevel + } + + /** + * Checks and logs any message. + * @param priority The priority of the log message. + * @param tag The tag associated with the log message. + * @param message The log message. + * @param t An optional throwable. + */ + override fun log(priority: Int, tag: String?, message: String, t: Throwable?) { + executor.execute { + logMessage(priority, tag.orEmpty(), message) + } + } + + /** + * Logs a message based on the priority. + * @param priority The priority of the log message. + * @param tag The tag associated with the log message. + * @param message The log message. + */ + private fun logMessage(priority: Int, tag: String, message: String) { + val messageWithTag = "[$tag] : $message" + when (priority) { + Log.VERBOSE -> logger.trace(messageWithTag) + Log.DEBUG -> logger.debug(messageWithTag) + Log.INFO -> logger.info(messageWithTag) + Log.WARN -> logger.warn(messageWithTag) + Log.ERROR, Log.ASSERT -> logger.error(messageWithTag) + } + } + + /** + * Checks if a particular log line should be logged in the file or not. + * @param priority The priority of the log message. + * @return True if the log message should be logged, false otherwise. + */ + @Deprecated("Deprecated in Java") + override fun isLoggable(priority: Int): Boolean { + return priority >= logLevel + } + + /** + * Configures the logger with a file size rolling policy (SizeBasedTriggeringPolicy). + * https://github.com/tony19/logback-android/wiki + * @param logDir The directory where logs should be stored. + */ + private fun configureLogger(logDir: String) { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + loggerContext.reset() + + val rollingFileAppender = RollingFileAppender().apply { + context = loggerContext + file = "$logDir/$logFileName.0.log" + } + + rollingPolicy = FixedWindowRollingPolicy().apply { + context = loggerContext + minIndex = 1 + maxIndex = 4 + setParent(rollingFileAppender) + fileNamePattern = "$logDir/$logFileName.%i.log" + start() + } + + val triggeringPolicy = SizeBasedTriggeringPolicy().apply { + context = loggerContext + maxFileSize = "$fileSizeInKb" + start() + } + + val encoder = PatternLayoutEncoder().apply { + context = loggerContext + pattern = "%-27(%date{ISO8601}) [%-5level] [%thread] %msg%n" + start() + } + + rollingFileAppender.apply { + this.encoder = encoder + rollingPolicy = rollingPolicy + this.triggeringPolicy = triggeringPolicy + start() + } + + val rootLogger = LoggerFactory.getLogger( + Logger.ROOT_LOGGER_NAME + ) as ch.qos.logback.classic.Logger + rootLogger.addAppender(rollingFileAppender) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java deleted file mode 100644 index 5eeca6d3ed..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.java +++ /dev/null @@ -1,8 +0,0 @@ -package fr.free.nrw.commons.logging; - -/** - * Can be implemented to set the log level for file tree - */ -public interface LogLevelSettableTree { - void setLogLevel(int logLevel); -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt new file mode 100644 index 0000000000..babe78121e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogLevelSettableTree.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.logging + +/** + * Can be implemented to set the log level for file tree + */ +interface LogLevelSettableTree { + fun setLogLevel(logLevel: Int) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java deleted file mode 100644 index c28b2145b7..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.logging; - -import android.os.Environment; - -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.utils.ConfigUtils; - -/** - * Returns the log directory - */ -public final class LogUtils { - private LogUtils() { - } - - /** - * Returns the directory for saving logs on the device - * - * @return - */ - public static String getLogDirectory() { - String dirPath; - if (ConfigUtils.isBetaFlavour()) { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta"; - } else { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod"; - } - - FileUtils.recursivelyCreateDirs(dirPath); - return dirPath; - } - - /** - * Returns the directory for saving logs on the device - * - * @return - */ - public static String getLogZipDirectory() { - String dirPath; - if (ConfigUtils.isBetaFlavour()) { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/beta/zip"; - } else { - dirPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) + "/logs/prod/zip"; - } - - FileUtils.recursivelyCreateDirs(dirPath); - return dirPath; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt new file mode 100644 index 0000000000..6c91d92dd6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogUtils.kt @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.logging + +import android.os.Environment + +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.utils.ConfigUtils + + +/** + * Returns the log directory + */ +object LogUtils { + + /** + * Returns the directory for saving logs on the device. + * + * @return The path to the log directory. + */ + fun getLogDirectory(): String { + val dirPath = if (ConfigUtils.isBetaFlavour) { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/beta" + } else { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/prod" + } + + FileUtils.recursivelyCreateDirs(dirPath) + return dirPath + } + + /** + * Returns the directory for saving zipped logs on the device. + * + * @return The path to the zipped log directory. + */ + fun getLogZipDirectory(): String { + val dirPath = if (ConfigUtils.isBetaFlavour) { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/beta/zip" + } else { + "${Environment + .getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS + )}/logs/prod/zip" + } + + FileUtils.recursivelyCreateDirs(dirPath) + return dirPath + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java deleted file mode 100644 index 68f7bd78c0..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.java +++ /dev/null @@ -1,201 +0,0 @@ -package fr.free.nrw.commons.logging; - -import static org.acra.ACRA.getErrorReporter; - -import android.content.Context; -import android.content.Intent; -import android.net.Uri; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; - -import org.acra.data.CrashReportData; -import org.acra.sender.ReportSender; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; - -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import timber.log.Timber; - -/** - * Abstract class that implements Acra's log sender - */ -public abstract class LogsSender implements ReportSender { - - String mailTo; - String logFileName; - String emailSubject; - String emailBody; - - private final SessionManager sessionManager; - - LogsSender(SessionManager sessionManager) { - this.sessionManager = sessionManager; - } - - /** - * Overrides send method of ACRA's ReportSender to send logs - * - * @param context - * @param report - */ - @Override - public void send(@NonNull final Context context, @Nullable CrashReportData report) { - sendLogs(context, report); - } - - /** - * Gets zipped log files and sends it via email. Can be modified to change the send log mechanism - * - * @param context - * @param report - */ - private void sendLogs(Context context, CrashReportData report) { - final Uri logFileUri = getZippedLogFileUri(context, report); - if (logFileUri != null) { - sendEmail(context, logFileUri); - } else { - getErrorReporter().handleSilentException(null); - } - } - - /*** - * Provides any extra information that you want to send. The return value will be - * delivered inside the report verbatim - * - * @return - */ - protected abstract String getExtraInfo(); - - /** - * Fires an intent to send email with logs - * - * @param context - * @param logFileUri - */ - private void sendEmail(Context context, Uri logFileUri) { - String subject = emailSubject; - String body = emailBody; - - Intent emailIntent = new Intent(Intent.ACTION_SEND); - emailIntent.setType("message/rfc822"); - emailIntent.putExtra(Intent.EXTRA_EMAIL, new String[]{mailTo}); - emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject); - emailIntent.putExtra(Intent.EXTRA_TEXT, body); - emailIntent.putExtra(Intent.EXTRA_STREAM, logFileUri); - emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using))); - } - - /** - * Returns the URI for the zipped log file - * - * @param report - * @return - */ - private Uri getZippedLogFileUri(Context context, CrashReportData report) { - try { - StringBuilder builder = new StringBuilder(); - if (report != null) { - attachCrashInfo(report, builder); - } - attachUserInfo(builder); - attachExtraInfo(builder); - byte[] metaData = builder.toString().getBytes(Charset.forName("UTF-8")); - File zipFile = new File(LogUtils.getLogZipDirectory(), logFileName); - writeLogToZipFile(metaData, zipFile); - return FileProvider - .getUriForFile(context, - context.getApplicationContext().getPackageName() + ".provider", zipFile); - } catch (IOException e) { - Timber.w(e, "Error in generating log file"); - } - return null; - } - - /** - * Checks if there are any pending crash reports and attaches them to the logs - * - * @param report - * @param builder - */ - private void attachCrashInfo(CrashReportData report, StringBuilder builder) { - if (report == null) { - return; - } - builder.append(report); - } - - /** - * Attaches username to the the meta_data file - * - * @param builder - */ - private void attachUserInfo(StringBuilder builder) { - builder.append("MediaWiki Username = ").append(sessionManager.getUserName()).append("\n"); - } - - /** - * Gets any extra meta information to be attached with the log files - * - * @param builder - */ - private void attachExtraInfo(StringBuilder builder) { - String infoToBeAttached = getExtraInfo(); - builder.append(infoToBeAttached); - builder.append("\n"); - } - - /** - * Zips the logs and meta information - * - * @param metaData - * @param zipFile - * @throws IOException - */ - private void writeLogToZipFile(byte[] metaData, File zipFile) throws IOException { - FileOutputStream fos = new FileOutputStream(zipFile); - BufferedOutputStream bos = new BufferedOutputStream(fos); - ZipOutputStream zos = new ZipOutputStream(bos); - File logDir = new File(LogUtils.getLogDirectory()); - - if (!logDir.exists() || logDir.listFiles().length == 0) { - return; - } - - byte[] buffer = new byte[1024]; - for (File file : logDir.listFiles()) { - if (file.isDirectory()) { - continue; - } - FileInputStream fis = new FileInputStream(file); - BufferedInputStream bis = new BufferedInputStream(fis); - zos.putNextEntry(new ZipEntry(file.getName())); - int length; - while ((length = bis.read(buffer)) > 0) { - zos.write(buffer, 0, length); - } - zos.closeEntry(); - bis.close(); - } - - //attach metadata as a separate file - zos.putNextEntry(new ZipEntry("meta_data.txt")); - zos.write(metaData); - zos.closeEntry(); - - zos.flush(); - zos.close(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt new file mode 100644 index 0000000000..cd6bb7d700 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/logging/LogsSender.kt @@ -0,0 +1,193 @@ +package fr.free.nrw.commons.logging + +import android.content.Context +import android.content.Intent +import android.net.Uri + +import androidx.core.content.FileProvider + +import org.acra.data.CrashReportData +import org.acra.sender.ReportSender + +import java.io.BufferedInputStream +import java.io.BufferedOutputStream +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream + +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import org.acra.ACRA.errorReporter +import timber.log.Timber + + +/** + * Abstract class that implements Acra's log sender. + */ +abstract class LogsSender( + private val sessionManager: SessionManager +): ReportSender { + + var mailTo: String? = null + var logFileName: String? = null + var emailSubject: String? = null + var emailBody: String? = null + + /** + * Overrides the send method of ACRA's ReportSender to send logs. + * + * @param context The context in which to send the logs. + * @param report The crash report data, if any. + */ + fun sendWithNullable(context: Context, report: CrashReportData?) { + if (report == null) { + errorReporter.handleSilentException(null) + return + } + send(context, report) + } + + override fun send(context: Context, report: CrashReportData) { + sendLogs(context, report) + } + + /** + * Gets zipped log files and sends them via email. Can be modified to change the send + * log mechanism. + * + * @param context The context in which to send the logs. + * @param report The crash report data, if any. + */ + private fun sendLogs(context: Context, report: CrashReportData?) { + val logFileUri = getZippedLogFileUri(context, report) + if (logFileUri != null) { + sendEmail(context, logFileUri) + } else { + errorReporter.handleSilentException(null) + + } + } + + /** + * Provides any extra information that you want to send. The return value will be + * delivered inside the report verbatim. + * + * @return A string containing the extra information. + */ + protected abstract fun getExtraInfo(): String + + /** + * Fires an intent to send an email with logs. + * + * @param context The context in which to send the email. + * @param logFileUri The URI of the zipped log file. + */ + private fun sendEmail(context: Context, logFileUri: Uri) { + val emailIntent = Intent(Intent.ACTION_SEND).apply { + type = "message/rfc822" + putExtra(Intent.EXTRA_EMAIL, arrayOf(mailTo)) + putExtra(Intent.EXTRA_SUBJECT, emailSubject) + putExtra(Intent.EXTRA_TEXT, emailBody) + putExtra(Intent.EXTRA_STREAM, logFileUri) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + context.startActivity(Intent.createChooser(emailIntent, context.getString(R.string.share_logs_using))) + } + + /** + * Returns the URI for the zipped log file. + * + * @param context The context for file URI generation. + * @param report The crash report data, if any. + * @return The URI of the zipped log file or null if an error occurs. + */ + private fun getZippedLogFileUri(context: Context, report: CrashReportData?): Uri? { + return try { + val builder = StringBuilder().apply { + report?.let { attachCrashInfo(it, this) } + attachUserInfo(this) + attachExtraInfo(this) + } + val metaData = builder.toString().toByteArray(Charsets.UTF_8) + val zipFile = File(LogUtils.getLogZipDirectory(), logFileName ?: "logs.zip") + writeLogToZipFile(metaData, zipFile) + FileProvider.getUriForFile( + context, + "${context.applicationContext.packageName}.provider", + zipFile + ) + } catch (e: IOException) { + Timber.w(e, "Error in generating log file") + null + } + } + + /** + * Checks if there are any pending crash reports and attaches them to the logs. + * + * @param report The crash report data, if any. + * @param builder The string builder to append crash info. + */ + private fun attachCrashInfo(report: CrashReportData?, builder: StringBuilder) { + if(report != null) { + builder.append(report) + } + } + + /** + * Attaches the username to the metadata file. + * + * @param builder The string builder to append user info. + */ + private fun attachUserInfo(builder: StringBuilder) { + builder.append("MediaWiki Username = ").append(sessionManager.userName).append("\n") + } + + /** + * Gets any extra metadata information to be attached with the log files. + * + * @param builder The string builder to append extra info. + */ + private fun attachExtraInfo(builder: StringBuilder) { + builder.append(getExtraInfo()).append("\n") + } + + /** + * Zips the logs and metadata information. + * + * @param metaData The metadata to be added to the zip file. + * @param zipFile The zip file to write to. + * @throws IOException If an I/O error occurs. + */ + @Throws(IOException::class) + private fun writeLogToZipFile(metaData: ByteArray, zipFile: File) { + val logDir = File(LogUtils.getLogDirectory()) + if (!logDir.exists() || logDir.listFiles().isNullOrEmpty()) return + + ZipOutputStream(BufferedOutputStream(FileOutputStream(zipFile))).use { zos -> + val buffer = ByteArray(1024) + logDir.listFiles()?.forEach { file -> + if (file.isDirectory) return@forEach + FileInputStream(file).use { fis -> + BufferedInputStream(fis).use { bis -> + zos.putNextEntry(ZipEntry(file.name)) + var length: Int + while (bis.read(buffer).also { length = it } > 0) { + zos.write(buffer, 0, length) + } + zos.closeEntry() + } + } + } + + // Attach metadata as a separate file. + zos.putNextEntry(ZipEntry("meta_data.txt")) + zos.write(metaData) + zos.closeEntry() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index b55ac60099..86ee5c4feb 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -1,6 +1,7 @@ package fr.free.nrw.commons.settings import android.Manifest.permission +import android.annotation.SuppressLint import android.app.Activity import android.app.Dialog import android.content.Context.MODE_PRIVATE @@ -527,7 +528,7 @@ class SettingsFragment : PreferenceFragmentCompat() { PermissionUtils.PERMISSIONS_STORAGE ) ) { - commonsLogSender.send(requireActivity(), null) + commonsLogSender.sendWithNullable(requireActivity(), null) } else { requestExternalStoragePermissions() } From dac3657536b0a258b29056e6022a399956dfc939 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 28 Nov 2024 23:01:29 -0600 Subject: [PATCH 042/231] Migrate kvstore to kotlin (#5973) * Get good test around the basic KvStore before starting conversion * Converted BasicKvStore to kotlin and removed dead code * Converted JsonKvStore to kotlin --------- Co-authored-by: Nicolas Raoul --- .../free/nrw/commons/auth/SessionManager.kt | 2 +- .../nrw/commons/kvstore/BasicKvStore.java | 215 ---------------- .../free/nrw/commons/kvstore/BasicKvStore.kt | 152 +++++++++++ .../free/nrw/commons/kvstore/JsonKvStore.java | 68 ----- .../free/nrw/commons/kvstore/JsonKvStore.kt | 52 ++++ .../nrw/commons/kvstore/KeyValueStore.java | 35 --- .../free/nrw/commons/kvstore/KeyValueStore.kt | 33 +++ .../nrw/commons/utils/SystemThemeUtils.kt | 2 +- .../nrw/commons/kvstore/BasicKvStoreTest.kt | 238 ++++++++++++++++++ .../nrw/commons/kvstore/JsonKvStoreTest.kt | 85 +++++++ 10 files changed, 562 insertions(+), 320 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java create mode 100644 app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt index eba4a55f41..c9eb7d2f13 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.kt @@ -62,7 +62,7 @@ class SessionManager @Inject constructor( fun forceLogin(context: Context?) = context?.let { LoginActivity.startYourself(it) } - fun getPreference(key: String?): Boolean = + fun getPreference(key: String): Boolean = defaultKvStore.getBoolean(key) fun logout(): Completable = Completable.fromObservable( diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java deleted file mode 100644 index 0328988960..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.java +++ /dev/null @@ -1,215 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import timber.log.Timber; - -public class BasicKvStore implements KeyValueStore { - private static final String KEY_VERSION = "__version__"; - /* - This class only performs puts, sets and clears. - A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will - require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed. - */ - private final SharedPreferences _store; - - public BasicKvStore(Context context, String storeName) { - _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE); - } - - /** - * If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0. - */ - public BasicKvStore(Context context, String storeName, int version) { - this(context,storeName,version,false); - } - - public BasicKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade) { - _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE); - int oldVersion = getInt(KEY_VERSION); - - if (version > oldVersion) { - Timber.i("version updated from %s to %s, with clearFlag %b", oldVersion, version, clearAllOnUpgrade); - onVersionUpdate(oldVersion, version, clearAllOnUpgrade); - } - - if (version < oldVersion) { - throw new IllegalArgumentException( - "kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " + - version); - } - //Keep this statement at the end so that clearing of store does not cause version also to get removed. - putIntInternal(KEY_VERSION, version); - } - - public void onVersionUpdate(int oldVersion, int version, boolean clearAllFlag) { - if(clearAllFlag) { - clearAll(); - } - } - - public Set getKeySet() { - Map allContents = new HashMap<>(_store.getAll()); - allContents.remove(KEY_VERSION); - return allContents.keySet(); - } - - @Nullable - public Map getAll() { - Map allContents = _store.getAll(); - if (allContents == null || allContents.size() == 0) { - return null; - } - allContents.remove(KEY_VERSION); - return new HashMap<>(allContents); - } - - @Override - public String getString(String key) { - return getString(key, null); - } - - @Override - public boolean getBoolean(String key) { - return getBoolean(key, false); - } - - @Override - public long getLong(String key) { - return getLong(key, 0); - } - - @Override - public int getInt(String key) { - return getInt(key, 0); - } - - @Override - public String getString(String key, String defaultValue) { - return _store.getString(key, defaultValue); - } - - @Override - public boolean getBoolean(String key, boolean defaultValue) { - return _store.getBoolean(key, defaultValue); - } - - @Override - public long getLong(String key, long defaultValue) { - return _store.getLong(key, defaultValue); - } - - @Override - public int getInt(String key, int defaultValue) { - return _store.getInt(key, defaultValue); - } - - public void putAllStrings(Map keyValuePairs) { - SharedPreferences.Editor editor = _store.edit(); - for (Map.Entry keyValuePair : keyValuePairs.entrySet()) { - putString(editor, keyValuePair.getKey(), keyValuePair.getValue(), false); - } - editor.apply(); - } - - @Override - public void putString(String key, String value) { - SharedPreferences.Editor editor = _store.edit(); - putString(editor, key, value, true); - } - - private void putString(SharedPreferences.Editor editor, String key, String value, - boolean commit) { - assertKeyNotReserved(key); - editor.putString(key, value); - if(commit) { - editor.apply(); - } - } - - @Override - public void putBoolean(String key, boolean value) { - assertKeyNotReserved(key); - SharedPreferences.Editor editor = _store.edit(); - editor.putBoolean(key, value); - editor.apply(); - } - - @Override - public void putLong(String key, long value) { - assertKeyNotReserved(key); - SharedPreferences.Editor editor = _store.edit(); - editor.putLong(key, value); - editor.apply(); - } - - @Override - public void putInt(String key, int value) { - assertKeyNotReserved(key); - putIntInternal(key, value); - } - - @Override - public boolean contains(String key) { - return _store.contains(key); - } - - @Override - public void remove(String key) { - SharedPreferences.Editor editor = _store.edit(); - editor.remove(key); - editor.apply(); - } - - @Override - public void clearAll() { - int version = getInt(KEY_VERSION); - SharedPreferences.Editor editor = _store.edit(); - editor.clear(); - editor.apply(); - putIntInternal(KEY_VERSION, version); - } - - @Override - public void clearAllWithVersion() { - SharedPreferences.Editor editor = _store.edit(); - editor.clear(); - editor.apply(); - } - - private void putIntInternal(String key, int value) { - SharedPreferences.Editor editor = _store.edit(); - editor.putInt(key, value); - editor.apply(); - } - - private void assertKeyNotReserved(String key) { - if (key.equals(KEY_VERSION)) { - throw new IllegalArgumentException(key + "is a reserved key"); - } - } - - public void registerChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) { - _store.registerOnSharedPreferenceChangeListener(l); - } - - public void unregisterChangeListener(SharedPreferences.OnSharedPreferenceChangeListener l) { - _store.unregisterOnSharedPreferenceChangeListener(l); - } - - public Set getStringSet(String key){ - return _store.getStringSet(key, new HashSet<>()); - } - - public void putStringSet(String key,Set value){ - _store.edit().putStringSet(key,value).apply(); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt new file mode 100644 index 0000000000..e0b8601648 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/BasicKvStore.kt @@ -0,0 +1,152 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import androidx.annotation.VisibleForTesting +import androidx.core.content.edit +import timber.log.Timber + +open class BasicKvStore : KeyValueStore { + /* + This class only performs puts, sets and clears. + A commit returns a boolean indicating whether it has succeeded, we are not throwing an exception as it will + require the dev to handle it in every usage - instead we will pass on this boolean so it can be evaluated if needed. + */ + private val _store: SharedPreferences + + constructor(context: Context, storeName: String?) { + _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE) + } + + /** + * If you don't want onVersionUpdate to be called on a fresh creation, the first version supplied for the kvstore should be set to 0. + */ + @JvmOverloads + constructor( + context: Context, + storeName: String?, + version: Int, + clearAllOnUpgrade: Boolean = false + ) { + _store = context.getSharedPreferences(storeName, Context.MODE_PRIVATE) + val oldVersion = _store.getInt(KEY_VERSION, 0) + + require(version >= oldVersion) { + "kvstore downgrade not allowed, old version:" + oldVersion + ", new version: " + + version + } + + if (version > oldVersion) { + Timber.i( + "version updated from %s to %s, with clearFlag %b", + oldVersion, + version, + clearAllOnUpgrade + ) + onVersionUpdate(oldVersion, version, clearAllOnUpgrade) + } + + //Keep this statement at the end so that clearing of store does not cause version also to get removed. + _store.edit { putInt(KEY_VERSION, version) } + } + + val all: Map? + get() { + val allContents = _store.all + if (allContents == null || allContents.isEmpty()) { + return null + } + allContents.remove(KEY_VERSION) + return HashMap(allContents) + } + + override fun getString(key: String): String? = + getString(key, null) + + override fun getBoolean(key: String): Boolean = + getBoolean(key, false) + + override fun getLong(key: String): Long = + getLong(key, 0) + + override fun getInt(key: String): Int = + getInt(key, 0) + + fun getStringSet(key: String?): MutableSet = + _store.getStringSet(key, HashSet())!! + + override fun getString(key: String, defaultValue: String?): String? = + _store.getString(key, defaultValue) + + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = + _store.getBoolean(key, defaultValue) + + override fun getLong(key: String, defaultValue: Long): Long = + _store.getLong(key, defaultValue) + + override fun getInt(key: String, defaultValue: Int): Int = + _store.getInt(key, defaultValue) + + fun putAllStrings(kvData: Map) = assertKeyNotReserved(kvData.keys) { + for ((key, value) in kvData) { + putString(key, value) + } + } + + override fun putString(key: String, value: String) = assertKeyNotReserved(key) { + putString(key, value) + } + + override fun putBoolean(key: String, value: Boolean) = assertKeyNotReserved(key) { + putBoolean(key, value) + } + + override fun putLong(key: String, value: Long) = assertKeyNotReserved(key) { + putLong(key, value) + } + + override fun putInt(key: String, value: Int) = assertKeyNotReserved(key) { + putInt(key, value) + } + + fun putStringSet(key: String?, value: Set?) = + _store.edit{ putStringSet(key, value) } + + override fun remove(key: String) = assertKeyNotReserved(key) { + remove(key) + } + + override fun contains(key: String): Boolean { + if (key == KEY_VERSION) return false + return _store.contains(key) + } + + override fun clearAll() { + val version = _store.getInt(KEY_VERSION, 0) + _store.edit { + clear() + putInt(KEY_VERSION, version) + } + } + + private fun onVersionUpdate(oldVersion: Int, version: Int, clearAllFlag: Boolean) { + if (clearAllFlag) { + clearAll() + } + } + + protected fun assertKeyNotReserved(key: Set, block: SharedPreferences.Editor.() -> Unit) { + key.forEach { require(it != KEY_VERSION) { "$it is a reserved key" } } + _store.edit { block(this) } + } + + protected fun assertKeyNotReserved(key: String, block: SharedPreferences.Editor.() -> Unit) { + require(key != KEY_VERSION) { "$key is a reserved key" } + _store.edit { block(this) } + } + + companion object { + @VisibleForTesting + const val KEY_VERSION: String = "__version__" + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java deleted file mode 100644 index d612880d9e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.java +++ /dev/null @@ -1,68 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -import android.content.Context; - -import androidx.annotation.Nullable; - -import com.google.gson.Gson; -import com.google.gson.JsonSyntaxException; - -import java.lang.reflect.Type; -import java.util.HashMap; -import java.util.Map; - -public class JsonKvStore extends BasicKvStore { - private final Gson gson; - - public JsonKvStore(Context context, String storeName, Gson gson) { - super(context, storeName); - this.gson = gson; - } - - public JsonKvStore(Context context, String storeName, int version, Gson gson) { - super(context, storeName, version); - this.gson = gson; - } - - public JsonKvStore(Context context, String storeName, int version, boolean clearAllOnUpgrade, Gson gson) { - super(context, storeName, version, clearAllOnUpgrade); - this.gson = gson; - } - - public void putAllJsons(Map jsonMap) { - Map stringsMap = new HashMap<>(jsonMap.size()); - for (Map.Entry keyValuePair : jsonMap.entrySet()) { - String jsonString = gson.toJson(keyValuePair.getValue()); - stringsMap.put(keyValuePair.getKey(), jsonString); - } - putAllStrings(stringsMap); - } - - public void putJson(String key, T object) { - putString(key, gson.toJson(object)); - } - - public void putJsonWithTypeInfo(String key, T object, Type type) { - putString(key, gson.toJson(object, type)); - } - - @Nullable - public T getJson(String key, Class clazz) { - String jsonString = getString(key); - try { - return gson.fromJson(jsonString, clazz); - } catch (JsonSyntaxException e) { - return null; - } - } - - @Nullable - public T getJson(String key, Type type) { - String jsonString = getString(key); - try { - return gson.fromJson(jsonString, type); - } catch (JsonSyntaxException e) { - return null; - } - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt new file mode 100644 index 0000000000..0f46222a4b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/JsonKvStore.kt @@ -0,0 +1,52 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException + +class JsonKvStore : BasicKvStore { + val gson: Gson + + constructor(context: Context, storeName: String?, gson: Gson) : super(context, storeName) { + this.gson = gson + } + + constructor(context: Context, storeName: String?, version: Int, gson: Gson) : super( + context, storeName, version + ) { + this.gson = gson + } + + constructor( + context: Context, + storeName: String?, + version: Int, + clearAllOnUpgrade: Boolean, + gson: Gson + ) : super(context, storeName, version, clearAllOnUpgrade) { + this.gson = gson + } + + fun putJson(key: String, value: T) = assertKeyNotReserved(key) { + putString(key, gson.toJson(value)) + } + + @Deprecated( + message = "Migrate to newer Kotlin syntax", + replaceWith = ReplaceWith("getJson(key)") + ) + fun getJson(key: String, clazz: Class?): T? = try { + gson.fromJson(getString(key), clazz) + } catch (e: JsonSyntaxException) { + null + } + + // Later, when the calls are coming from Kotlin, this will allow us to + // drop the "clazz" parameter, and just pick up the type at the call site. + // The deprecation warning should help migration! + inline fun getJson(key: String): T? = try { + gson.fromJson(getString(key), T::class.java) + } catch (e: JsonSyntaxException) { + null + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java deleted file mode 100644 index 46d6d8f813..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.free.nrw.commons.kvstore; - -public interface KeyValueStore { - String getString(String key); - - boolean getBoolean(String key); - - long getLong(String key); - - int getInt(String key); - - String getString(String key, String defaultValue); - - boolean getBoolean(String key, boolean defaultValue); - - long getLong(String key, long defaultValue); - - int getInt(String key, int defaultValue); - - void putString(String key, String value); - - void putBoolean(String key, boolean value); - - void putLong(String key, long value); - - void putInt(String key, int value); - - boolean contains(String key); - - void remove(String key); - - void clearAll(); - - void clearAllWithVersion(); -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt new file mode 100644 index 0000000000..6e19901cbe --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/kvstore/KeyValueStore.kt @@ -0,0 +1,33 @@ +package fr.free.nrw.commons.kvstore + +interface KeyValueStore { + fun getString(key: String): String? + + fun getBoolean(key: String): Boolean + + fun getLong(key: String): Long + + fun getInt(key: String): Int + + fun getString(key: String, defaultValue: String?): String? + + fun getBoolean(key: String, defaultValue: Boolean): Boolean + + fun getLong(key: String, defaultValue: Long): Long + + fun getInt(key: String, defaultValue: Int): Int + + fun putString(key: String, value: String) + + fun putBoolean(key: String, value: Boolean) + + fun putLong(key: String, value: Long) + + fun putInt(key: String, value: Int) + + fun contains(key: String): Boolean + + fun remove(key: String) + + fun clearAll() +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt index f4b1f2625d..87a710424a 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/SystemThemeUtils.kt @@ -46,7 +46,7 @@ class SystemThemeUtils @Inject constructor( // Returns true if the device is in night mode or false otherwise fun isDeviceInNightMode(): Boolean { return getSystemDefaultThemeBool( - applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme()) + applicationKvStore.getString(Prefs.KEY_THEME_VALUE, getSystemDefaultTheme())!! ) } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt new file mode 100644 index 0000000000..99fdf915b7 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/BasicKvStoreTest.kt @@ -0,0 +1,238 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock + +class BasicKvStoreTest { + private val context = mock() + private val prefs = mock() + private val editor = mock() + private lateinit var store: BasicKvStore + + @Before + fun setUp() { + whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs) + whenever(prefs.edit()).thenReturn(editor) + store = BasicKvStore(context, "name") + } + + @Test + fun versionUpdate() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99) + BasicKvStore(context, "name", 100, true) + + // It should clear itself and automatically put the new version number + verify(prefs, atLeast(2)).edit() + verify(editor).clear() + verify(editor).putInt(KEY_VERSION, 100) + verify(editor, atLeast(2)).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun versionDowngradeNotAllowed() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(100) + BasicKvStore(context, "name", 99, true) + } + + @Test + fun versionRedactedFromGetAll() { + val all = mutableMapOf("key" to "value", KEY_VERSION to 100) + whenever(prefs.all).thenReturn(all) + + val result = store.all + Assert.assertEquals(mapOf("key" to "value"), result) + } + + @Test + fun getAllHandlesNull() { + whenever(prefs.all).thenReturn(null) + Assert.assertNull(store.all) + } + + @Test + fun getAllHandlesEmpty() { + whenever(prefs.all).thenReturn(emptyMap()) + Assert.assertNull(store.all) + } + + @Test + fun getString() { + whenever(prefs.getString("key", null)).thenReturn("value") + Assert.assertEquals("value", store.getString("key")) + } + + @Test + fun getBoolean() { + whenever(prefs.getBoolean("key", false)).thenReturn(true) + Assert.assertTrue(store.getBoolean("key")) + } + + @Test + fun getLong() { + whenever(prefs.getLong("key", 0L)).thenReturn(100) + Assert.assertEquals(100L, store.getLong("key")) + } + + @Test + fun getInt() { + whenever(prefs.getInt("key", 0)).thenReturn(100) + Assert.assertEquals(100, store.getInt("key")) + } + + @Test + fun getStringWithDefault() { + whenever(prefs.getString("key", "junk")).thenReturn("value") + Assert.assertEquals("value", store.getString("key", "junk")) + } + + @Test + fun getBooleanWithDefault() { + whenever(prefs.getBoolean("key", true)).thenReturn(true) + Assert.assertTrue(store.getBoolean("key", true)) + } + + @Test + fun getLongWithDefault() { + whenever(prefs.getLong("key", 22L)).thenReturn(100) + Assert.assertEquals(100L, store.getLong("key", 22L)) + } + + @Test + fun getIntWithDefault() { + whenever(prefs.getInt("key", 22)).thenReturn(100) + Assert.assertEquals(100, store.getInt("key", 22)) + } + + @Test + fun putAllStrings() { + store.putAllStrings( + mapOf( + "one" to "fish", + "two" to "fish", + "red" to "fish", + "blue" to "fish" + ) + ) + + verify(prefs).edit() + verify(editor).putString("one", "fish") + verify(editor).putString("two", "fish") + verify(editor).putString("red", "fish") + verify(editor).putString("blue", "fish") + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putAllStringsWithReservedKey() { + store.putAllStrings( + mapOf( + "this" to "that", + KEY_VERSION to "something" + ) + ) + } + + @Test + fun putString() { + store.putString("this" , "that") + + verify(prefs).edit() + verify(editor).putString("this", "that") + verify(editor).apply() + } + + @Test + fun putBoolean() { + store.putBoolean("this" , true) + + verify(prefs).edit() + verify(editor).putBoolean("this", true) + verify(editor).apply() + } + + @Test + fun putLong() { + store.putLong("this" , 123L) + + verify(prefs).edit() + verify(editor).putLong("this", 123L) + verify(editor).apply() + } + + @Test + fun putInt() { + store.putInt("this" , 16) + + verify(prefs).edit() + verify(editor).putInt("this", 16) + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putStringWithReservedKey() { + store.putString(KEY_VERSION, "that") + } + + @Test(expected = IllegalArgumentException::class) + fun putBooleanWithReservedKey() { + store.putBoolean(KEY_VERSION, true) + } + + @Test(expected = IllegalArgumentException::class) + fun putLongWithReservedKey() { + store.putLong(KEY_VERSION, 33L) + } + + @Test(expected = IllegalArgumentException::class) + fun putIntWithReservedKey() { + store.putInt(KEY_VERSION, 12) + } + + @Test + fun testContains() { + whenever(prefs.contains("key")).thenReturn(true) + Assert.assertTrue(store.contains("key")) + } + + @Test + fun containsRedactsVersion() { + whenever(prefs.contains(KEY_VERSION)).thenReturn(true) + Assert.assertFalse(store.contains(KEY_VERSION)) + } + + @Test + fun remove() { + store.remove("key") + + verify(prefs).edit() + verify(editor).remove("key") + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun removeWithReservedKey() { + store.remove(KEY_VERSION) + } + + @Test + fun clearAllPreservesVersion() { + whenever(prefs.getInt(KEY_VERSION, 0)).thenReturn(99) + + store.clearAll() + + verify(prefs).edit() + verify(editor).clear() + verify(editor).putInt(KEY_VERSION, 99) + verify(editor).apply() + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt new file mode 100644 index 0000000000..0a0bdfc473 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/kvstore/JsonKvStoreTest.kt @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.kvstore + +import android.content.Context +import android.content.SharedPreferences +import com.google.gson.Gson +import com.nhaarman.mockitokotlin2.atLeast +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import fr.free.nrw.commons.kvstore.BasicKvStore.Companion.KEY_VERSION +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mockito.mock + +class JsonKvStoreTest { + private val context = mock() + private val prefs = mock() + private val editor = mock() + + private val gson = Gson() + private val testData = Person(16, "Bob", true, Pet("Poodle", 2)) + private val expected = gson.toJson(testData) + + private lateinit var store: JsonKvStore + + @Before + fun setUp() { + whenever(context.getSharedPreferences(anyString(), anyInt())).thenReturn(prefs) + whenever(prefs.edit()).thenReturn(editor) + store = JsonKvStore(context, "name", gson) + } + + @Test + fun putJson() { + store.putJson("person", testData) + + verify(prefs).edit() + verify(editor).putString("person", expected) + verify(editor).apply() + } + + @Test(expected = IllegalArgumentException::class) + fun putJsonWithReservedKey() { + store.putJson(KEY_VERSION, testData) + } + + @Test + fun getJson() { + whenever(prefs.getString("key", null)).thenReturn(expected) + + val result = store.getJson("key", Person::class.java) + + Assert.assertEquals(testData, result) + } + + @Test + fun getJsonInTheFuture() { + whenever(prefs.getString("key", null)).thenReturn(expected) + + val resultOne: Person? = store.getJson("key") + Assert.assertEquals(testData, resultOne) + + val resultTwo = store.getJson("key") + Assert.assertEquals(testData, resultTwo) + } + + @Test + fun getJsonHandlesMalformedJson() { + whenever(prefs.getString("key", null)).thenReturn("junk") + + val result = store.getJson("key", Person::class.java) + + Assert.assertNull(result) + } + + data class Person( + val age: Int, val name: String, val hasPets: Boolean, val pet: Pet? + ) + + data class Pet( + val breed: String, val age: Int + ) +} \ No newline at end of file From 1e5521b434c5798440e115320fefc72e708d741a Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 29 Nov 2024 19:50:42 -0600 Subject: [PATCH 043/231] Convert dependency inject ("di") package to kotlin (#5976) * Convert a batch of easier modules * Convert the NetworkingModule to kotlin * Converted the ApplicationlessInjection to kotlin * Convert CommonsDaggerAppCompatActivity to kotlin * Convert CommonsDaggerContentProvider to kotlin * Convert CommonsDaggerIntentService to kotlin * Convert CommonsDaggerService to kotlin * Convert CommonsDaggerSupportFragment to kotlin * Convert CommonsDaggerBroadcastReceiver to kotlin * Convert CommonsApplicationModule to kotlin * Fix imports and make them consistent --- .../free/nrw/commons/actions/ThanksClient.kt | 2 +- .../commons/campaigns/CampaignsPresenter.kt | 5 +- .../ContributionBoundaryCallback.kt | 3 +- .../ContributionsListPresenter.java | 4 +- .../contributions/ContributionsPresenter.java | 3 +- .../ContributionsRemoteDataSource.kt | 4 +- .../fr/free/nrw/commons/db/Converters.java | 5 +- .../nrw/commons/di/ActivityBuilderModule.java | 90 ----- .../nrw/commons/di/ActivityBuilderModule.kt | 89 +++++ .../commons/di/ApplicationlessInjection.java | 105 ------ .../commons/di/ApplicationlessInjection.kt | 98 +++++ .../di/CommonsApplicationComponent.java | 85 ----- .../commons/di/CommonsApplicationComponent.kt | 80 ++++ .../commons/di/CommonsApplicationModule.java | 314 ---------------- .../commons/di/CommonsApplicationModule.kt | 239 ++++++++++++ .../di/CommonsDaggerAppCompatActivity.java | 48 --- .../di/CommonsDaggerAppCompatActivity.kt | 37 ++ .../di/CommonsDaggerBroadcastReceiver.java | 35 -- .../di/CommonsDaggerBroadcastReceiver.kt | 25 ++ .../di/CommonsDaggerContentProvider.java | 32 -- .../di/CommonsDaggerContentProvider.kt | 20 + .../di/CommonsDaggerIntentService.java | 32 -- .../commons/di/CommonsDaggerIntentService.kt | 20 + .../nrw/commons/di/CommonsDaggerService.java | 31 -- .../nrw/commons/di/CommonsDaggerService.kt | 20 + .../di/CommonsDaggerSupportFragment.java | 75 ---- .../di/CommonsDaggerSupportFragment.kt | 66 ++++ .../di/ContentProviderBuilderModule.java | 38 -- .../di/ContentProviderBuilderModule.kt | 37 ++ .../nrw/commons/di/FragmentBuilderModule.java | 166 --------- .../nrw/commons/di/FragmentBuilderModule.kt | 165 +++++++++ .../free/nrw/commons/di/NetworkingModule.java | 350 ------------------ .../free/nrw/commons/di/NetworkingModule.kt | 316 ++++++++++++++++ .../nrw/commons/di/ServiceBuilderModule.java | 19 - .../nrw/commons/di/ServiceBuilderModule.kt | 17 + .../free/nrw/commons/mwapi/CategoryApi.java | 15 +- .../upload/PendingUploadsPresenter.java | 3 +- .../upload/categories/CategoriesPresenter.kt | 6 +- .../upload/depicts/DepictsPresenter.kt | 6 +- .../commons/wikidata/CommonsServiceFactory.kt | 18 +- .../nrw/commons/TestCommonsApplication.kt | 16 +- 41 files changed, 1274 insertions(+), 1465 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java create mode 100644 app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt diff --git a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt index af305c9c6a..1dcf93edf4 100644 --- a/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt +++ b/app/src/main/java/fr/free/nrw/commons/actions/ThanksClient.kt @@ -3,7 +3,7 @@ package fr.free.nrw.commons.actions import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException -import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF +import fr.free.nrw.commons.di.NetworkingModule.Companion.NAMED_COMMONS_CSRF import io.reactivex.Observable import javax.inject.Inject import javax.inject.Named diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt index 3753dfb670..ffbf925406 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt @@ -3,9 +3,8 @@ package fr.free.nrw.commons.campaigns import android.annotation.SuppressLint import fr.free.nrw.commons.BasePresenter import fr.free.nrw.commons.campaigns.models.Campaign -import fr.free.nrw.commons.di.CommonsApplicationModule -import fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD -import fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient import fr.free.nrw.commons.utils.CommonsDateUtil.getIso8601DateFormatShort import io.reactivex.Scheduler diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt index 286abb97aa..3f7bffe91b 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionBoundaryCallback.kt @@ -3,6 +3,7 @@ package fr.free.nrw.commons.contributions import androidx.paging.PagedList.BoundaryCallback import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD import fr.free.nrw.commons.media.MediaClient import io.reactivex.Scheduler import io.reactivex.disposables.CompositeDisposable @@ -20,7 +21,7 @@ class ContributionBoundaryCallback private val repository: ContributionsRepository, private val sessionManager: SessionManager, private val mediaClient: MediaClient, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler, ) : BoundaryCallback() { private val compositeDisposable: CompositeDisposable = CompositeDisposable() var userName: String? = null diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java index 42495889d8..735ff63d43 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListPresenter.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.contributions; +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; + import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.paging.DataSource; @@ -34,7 +36,7 @@ public class ContributionsListPresenter implements UserActionListener { final ContributionBoundaryCallback contributionBoundaryCallback, final ContributionsRemoteDataSource contributionsRemoteDataSource, final ContributionsRepository repository, - @Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) { + @Named(IO_THREAD) final Scheduler ioThreadScheduler) { this.contributionBoundaryCallback = contributionBoundaryCallback; this.repository = repository; this.ioThreadScheduler = ioThreadScheduler; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java index 297a666169..495a4bc647 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsPresenter.java @@ -1,5 +1,6 @@ package fr.free.nrw.commons.contributions; +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import androidx.work.ExistingWorkPolicy; @@ -31,7 +32,7 @@ public class ContributionsPresenter implements UserActionListener { @Inject ContributionsPresenter(ContributionsRepository repository, UploadRepository uploadRepository, - @Named(CommonsApplicationModule.IO_THREAD) Scheduler ioThreadScheduler) { + @Named(IO_THREAD) Scheduler ioThreadScheduler) { this.contributionsRepository = repository; this.uploadRepository = uploadRepository; this.ioThreadScheduler = ioThreadScheduler; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt index 346c83b344..e8ff01b3e5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsRemoteDataSource.kt @@ -1,7 +1,7 @@ package fr.free.nrw.commons.contributions import androidx.paging.ItemKeyedDataSource -import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD import fr.free.nrw.commons.media.MediaClient import io.reactivex.Scheduler import io.reactivex.disposables.CompositeDisposable @@ -16,7 +16,7 @@ class ContributionsRemoteDataSource @Inject constructor( private val mediaClient: MediaClient, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioThreadScheduler: Scheduler, ) : ItemKeyedDataSource() { private val compositeDisposable: CompositeDisposable = CompositeDisposable() var userName: String? = null diff --git a/app/src/main/java/fr/free/nrw/commons/db/Converters.java b/app/src/main/java/fr/free/nrw/commons/db/Converters.java index a70cdc8154..c0f85420f5 100644 --- a/app/src/main/java/fr/free/nrw/commons/db/Converters.java +++ b/app/src/main/java/fr/free/nrw/commons/db/Converters.java @@ -22,7 +22,10 @@ public class Converters { public static Gson getGson() { - return ApplicationlessInjection.getInstance(CommonsApplication.getInstance()).getCommonsApplicationComponent().gson(); + return ApplicationlessInjection + .getInstance(CommonsApplication.getInstance()) + .getCommonsApplicationComponent() + .gson(); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java deleted file mode 100644 index 4516d806f2..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ /dev/null @@ -1,90 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.AboutActivity; -import fr.free.nrw.commons.LocationPicker.LocationPickerActivity; -import fr.free.nrw.commons.WelcomeActivity; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.auth.SignupActivity; -import fr.free.nrw.commons.category.CategoryDetailsActivity; -import fr.free.nrw.commons.contributions.MainActivity; -import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; -import fr.free.nrw.commons.description.DescriptionEditActivity; -import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity; -import fr.free.nrw.commons.explore.SearchActivity; -import fr.free.nrw.commons.media.ZoomableActivity; -import fr.free.nrw.commons.nearby.WikidataFeedback; -import fr.free.nrw.commons.notification.NotificationActivity; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.review.ReviewActivity; -import fr.free.nrw.commons.settings.SettingsActivity; -import fr.free.nrw.commons.upload.UploadActivity; -import fr.free.nrw.commons.upload.UploadProgressActivity; - -/** - * This Class handles the dependency injection (using dagger) - * so, if a developer needs to add a new activity to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class ActivityBuilderModule { - - @ContributesAndroidInjector - abstract LoginActivity bindLoginActivity(); - - @ContributesAndroidInjector - abstract WelcomeActivity bindWelcomeActivity(); - - @ContributesAndroidInjector - abstract MainActivity bindContributionsActivity(); - - @ContributesAndroidInjector - abstract CustomSelectorActivity bindCustomSelectorActivity(); - - @ContributesAndroidInjector - abstract SettingsActivity bindSettingsActivity(); - - @ContributesAndroidInjector - abstract AboutActivity bindAboutActivity(); - - @ContributesAndroidInjector - abstract LocationPickerActivity bindLocationPickerActivity(); - - @ContributesAndroidInjector - abstract SignupActivity bindSignupActivity(); - - @ContributesAndroidInjector - abstract NotificationActivity bindNotificationActivity(); - - @ContributesAndroidInjector - abstract UploadActivity bindUploadActivity(); - - @ContributesAndroidInjector - abstract SearchActivity bindSearchActivity(); - - @ContributesAndroidInjector - abstract CategoryDetailsActivity bindCategoryDetailsActivity(); - - @ContributesAndroidInjector - abstract WikidataItemDetailsActivity bindDepictionDetailsActivity(); - - @ContributesAndroidInjector - abstract ProfileActivity bindAchievementsActivity(); - - @ContributesAndroidInjector - abstract ReviewActivity bindReviewActivity(); - - @ContributesAndroidInjector - abstract DescriptionEditActivity bindDescriptionEditActivity(); - - @ContributesAndroidInjector - abstract ZoomableActivity bindZoomableActivity(); - - @ContributesAndroidInjector - abstract UploadProgressActivity bindUploadProgressActivity(); - - @ContributesAndroidInjector - abstract WikidataFeedback bindWikiFeedback(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt new file mode 100644 index 0000000000..86750a5536 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.kt @@ -0,0 +1,89 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.AboutActivity +import fr.free.nrw.commons.LocationPicker.LocationPickerActivity +import fr.free.nrw.commons.WelcomeActivity +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.auth.SignupActivity +import fr.free.nrw.commons.category.CategoryDetailsActivity +import fr.free.nrw.commons.contributions.MainActivity +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import fr.free.nrw.commons.description.DescriptionEditActivity +import fr.free.nrw.commons.explore.SearchActivity +import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity +import fr.free.nrw.commons.media.ZoomableActivity +import fr.free.nrw.commons.nearby.WikidataFeedback +import fr.free.nrw.commons.notification.NotificationActivity +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.review.ReviewActivity +import fr.free.nrw.commons.settings.SettingsActivity +import fr.free.nrw.commons.upload.UploadActivity +import fr.free.nrw.commons.upload.UploadProgressActivity + +/** + * This Class handles the dependency injection (using dagger) + * so, if a developer needs to add a new activity to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class ActivityBuilderModule { + @ContributesAndroidInjector + abstract fun bindLoginActivity(): LoginActivity + + @ContributesAndroidInjector + abstract fun bindWelcomeActivity(): WelcomeActivity + + @ContributesAndroidInjector + abstract fun bindContributionsActivity(): MainActivity + + @ContributesAndroidInjector + abstract fun bindCustomSelectorActivity(): CustomSelectorActivity + + @ContributesAndroidInjector + abstract fun bindSettingsActivity(): SettingsActivity + + @ContributesAndroidInjector + abstract fun bindAboutActivity(): AboutActivity + + @ContributesAndroidInjector + abstract fun bindLocationPickerActivity(): LocationPickerActivity + + @ContributesAndroidInjector + abstract fun bindSignupActivity(): SignupActivity + + @ContributesAndroidInjector + abstract fun bindNotificationActivity(): NotificationActivity + + @ContributesAndroidInjector + abstract fun bindUploadActivity(): UploadActivity + + @ContributesAndroidInjector + abstract fun bindSearchActivity(): SearchActivity + + @ContributesAndroidInjector + abstract fun bindCategoryDetailsActivity(): CategoryDetailsActivity + + @ContributesAndroidInjector + abstract fun bindDepictionDetailsActivity(): WikidataItemDetailsActivity + + @ContributesAndroidInjector + abstract fun bindAchievementsActivity(): ProfileActivity + + @ContributesAndroidInjector + abstract fun bindReviewActivity(): ReviewActivity + + @ContributesAndroidInjector + abstract fun bindDescriptionEditActivity(): DescriptionEditActivity + + @ContributesAndroidInjector + abstract fun bindZoomableActivity(): ZoomableActivity + + @ContributesAndroidInjector + abstract fun bindUploadProgressActivity(): UploadProgressActivity + + @ContributesAndroidInjector + abstract fun bindWikiFeedback(): WikidataFeedback +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java deleted file mode 100644 index f2bff5db72..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.java +++ /dev/null @@ -1,105 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.ContentProvider; -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import dagger.android.HasAndroidInjector; -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasActivityInjector; -import dagger.android.HasBroadcastReceiverInjector; -import dagger.android.HasContentProviderInjector; -import dagger.android.HasFragmentInjector; -import dagger.android.HasServiceInjector; -import dagger.android.support.HasSupportFragmentInjector; - -/** - * Provides injectors for all sorts of components - * Ex: Activities, Fragments, Services, ContentProviders - */ -public class ApplicationlessInjection - implements - HasAndroidInjector, - HasActivityInjector, - HasFragmentInjector, - HasSupportFragmentInjector, - HasServiceInjector, - HasBroadcastReceiverInjector, - HasContentProviderInjector { - - private static ApplicationlessInjection instance = null; - - @Inject DispatchingAndroidInjector androidInjector; - @Inject DispatchingAndroidInjector activityInjector; - @Inject DispatchingAndroidInjector broadcastReceiverInjector; - @Inject DispatchingAndroidInjector fragmentInjector; - @Inject DispatchingAndroidInjector supportFragmentInjector; - @Inject DispatchingAndroidInjector serviceInjector; - @Inject DispatchingAndroidInjector contentProviderInjector; - - private CommonsApplicationComponent commonsApplicationComponent; - - public ApplicationlessInjection(Context applicationContext) { - commonsApplicationComponent = DaggerCommonsApplicationComponent.builder() - .appModule(new CommonsApplicationModule(applicationContext)).build(); - commonsApplicationComponent.inject(this); - } - - @Override - public AndroidInjector androidInjector() { - return androidInjector; - } - - @Override - public DispatchingAndroidInjector activityInjector() { - return activityInjector; - } - - @Override - public DispatchingAndroidInjector fragmentInjector() { - return fragmentInjector; - } - - @Override - public DispatchingAndroidInjector supportFragmentInjector() { - return supportFragmentInjector; - } - - @Override - public DispatchingAndroidInjector broadcastReceiverInjector() { - return broadcastReceiverInjector; - } - - @Override - public DispatchingAndroidInjector serviceInjector() { - return serviceInjector; - } - - @Override - public AndroidInjector contentProviderInjector() { - return contentProviderInjector; - } - - public CommonsApplicationComponent getCommonsApplicationComponent() { - return commonsApplicationComponent; - } - - public static ApplicationlessInjection getInstance(Context applicationContext) { - if (instance == null) { - synchronized (ApplicationlessInjection.class) { - if (instance == null) { - instance = new ApplicationlessInjection(applicationContext); - } - } - } - - return instance; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt new file mode 100644 index 0000000000..1a88bd8091 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ApplicationlessInjection.kt @@ -0,0 +1,98 @@ +package fr.free.nrw.commons.di + +import android.app.Activity +import android.app.Fragment +import android.app.Service +import android.content.BroadcastReceiver +import android.content.ContentProvider +import android.content.Context +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasActivityInjector +import dagger.android.HasAndroidInjector +import dagger.android.HasBroadcastReceiverInjector +import dagger.android.HasContentProviderInjector +import dagger.android.HasFragmentInjector +import dagger.android.HasServiceInjector +import dagger.android.support.HasSupportFragmentInjector +import javax.inject.Inject +import androidx.fragment.app.Fragment as AndroidXFragmen + +/** + * Provides injectors for all sorts of components + * Ex: Activities, Fragments, Services, ContentProviders + */ +class ApplicationlessInjection(applicationContext: Context) : HasAndroidInjector, + HasActivityInjector, HasFragmentInjector, HasSupportFragmentInjector, HasServiceInjector, + HasBroadcastReceiverInjector, HasContentProviderInjector { + @Inject @JvmField + var androidInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var activityInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var broadcastReceiverInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var fragmentInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var supportFragmentInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var serviceInjector: DispatchingAndroidInjector? = null + + @Inject @JvmField + var contentProviderInjector: DispatchingAndroidInjector? = null + + val instance: ApplicationlessInjection get() = _instance!! + + val commonsApplicationComponent: CommonsApplicationComponent = + DaggerCommonsApplicationComponent + .builder() + .appModule(CommonsApplicationModule(applicationContext)) + .build() + + init { + commonsApplicationComponent.inject(this) + } + + override fun androidInjector(): AndroidInjector? = + androidInjector + + override fun activityInjector(): DispatchingAndroidInjector? = + activityInjector + + override fun fragmentInjector(): DispatchingAndroidInjector? = + fragmentInjector + + override fun supportFragmentInjector(): DispatchingAndroidInjector? = + supportFragmentInjector + + override fun broadcastReceiverInjector(): DispatchingAndroidInjector? = + broadcastReceiverInjector + + override fun serviceInjector(): DispatchingAndroidInjector? = + serviceInjector + + override fun contentProviderInjector(): AndroidInjector? = + contentProviderInjector + + companion object { + private var _instance: ApplicationlessInjection? = null + + @JvmStatic + fun getInstance(applicationContext: Context): ApplicationlessInjection { + if (_instance == null) { + synchronized(ApplicationlessInjection::class.java) { + if (_instance == null) { + _instance = ApplicationlessInjection(applicationContext) + } + } + } + + return _instance!! + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java deleted file mode 100644 index 0d847b6493..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ /dev/null @@ -1,85 +0,0 @@ -package fr.free.nrw.commons.di; - -import com.google.gson.Gson; - -import fr.free.nrw.commons.explore.categories.CategoriesModule; -import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; -import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; -import fr.free.nrw.commons.nearby.NearbyController; -import fr.free.nrw.commons.upload.worker.UploadWorker; -import javax.inject.Singleton; - -import dagger.Component; -import dagger.android.AndroidInjectionModule; -import dagger.android.AndroidInjector; -import dagger.android.support.AndroidSupportInjectionModule; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.contributions.ContributionsModule; -import fr.free.nrw.commons.explore.depictions.DepictionModule; -import fr.free.nrw.commons.explore.SearchModule; -import fr.free.nrw.commons.review.ReviewController; -import fr.free.nrw.commons.settings.SettingsFragment; -import fr.free.nrw.commons.upload.FileProcessor; -import fr.free.nrw.commons.upload.UploadModule; -import fr.free.nrw.commons.widget.PicOfDayAppWidget; - - -/** - * Facilitates Injection from CommonsApplicationModule to all the - * classes seeking a dependency to be injected - */ -@Singleton -@Component(modules = { - CommonsApplicationModule.class, - NetworkingModule.class, - AndroidInjectionModule.class, - AndroidSupportInjectionModule.class, - ActivityBuilderModule.class, - FragmentBuilderModule.class, - ServiceBuilderModule.class, - ContentProviderBuilderModule.class, - UploadModule.class, - ContributionsModule.class, - SearchModule.class, - DepictionModule.class, - CategoriesModule.class -}) -public interface CommonsApplicationComponent extends AndroidInjector { - void inject(CommonsApplication application); - - void inject(UploadWorker worker); - - void inject(LoginActivity activity); - - void inject(SettingsFragment fragment); - - void inject(MoreBottomSheetFragment fragment); - - void inject(MoreBottomSheetLoggedOutFragment fragment); - - void inject(ReviewController reviewController); - - //void inject(NavTabLayout view); - - @Override - void inject(ApplicationlessInjection instance); - - void inject(FileProcessor fileProcessor); - - void inject(PicOfDayAppWidget picOfDayAppWidget); - - @Singleton - void inject(NearbyController nearbyController); - - Gson gson(); - - @Component.Builder - @SuppressWarnings({"WeakerAccess", "unused"}) - interface Builder { - - Builder appModule(CommonsApplicationModule applicationModule); - - CommonsApplicationComponent build(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt new file mode 100644 index 0000000000..b0c0c4d37e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.kt @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.di + +import com.google.gson.Gson +import dagger.Component +import dagger.android.AndroidInjectionModule +import dagger.android.AndroidInjector +import dagger.android.support.AndroidSupportInjectionModule +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.auth.LoginActivity +import fr.free.nrw.commons.contributions.ContributionsModule +import fr.free.nrw.commons.explore.SearchModule +import fr.free.nrw.commons.explore.categories.CategoriesModule +import fr.free.nrw.commons.explore.depictions.DepictionModule +import fr.free.nrw.commons.navtab.MoreBottomSheetFragment +import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment +import fr.free.nrw.commons.nearby.NearbyController +import fr.free.nrw.commons.review.ReviewController +import fr.free.nrw.commons.settings.SettingsFragment +import fr.free.nrw.commons.upload.FileProcessor +import fr.free.nrw.commons.upload.UploadModule +import fr.free.nrw.commons.upload.worker.UploadWorker +import fr.free.nrw.commons.widget.PicOfDayAppWidget +import javax.inject.Singleton + +/** + * Facilitates Injection from CommonsApplicationModule to all the + * classes seeking a dependency to be injected + */ +@Singleton +@Component( + modules = [ + CommonsApplicationModule::class, + NetworkingModule::class, + AndroidInjectionModule::class, + AndroidSupportInjectionModule::class, + ActivityBuilderModule::class, + FragmentBuilderModule::class, + ServiceBuilderModule::class, + ContentProviderBuilderModule::class, + UploadModule::class, + ContributionsModule::class, + SearchModule::class, + DepictionModule::class, + CategoriesModule::class + ] +) +interface CommonsApplicationComponent : AndroidInjector { + fun inject(application: CommonsApplication) + + fun inject(worker: UploadWorker) + + fun inject(activity: LoginActivity) + + fun inject(fragment: SettingsFragment) + + fun inject(fragment: MoreBottomSheetFragment) + + fun inject(fragment: MoreBottomSheetLoggedOutFragment) + + fun inject(reviewController: ReviewController) + + override fun inject(instance: ApplicationlessInjection) + + fun inject(fileProcessor: FileProcessor) + + fun inject(picOfDayAppWidget: PicOfDayAppWidget) + + @Singleton + fun inject(nearbyController: NearbyController) + + fun gson(): Gson + + @Component.Builder + @Suppress("unused") + interface Builder { + fun appModule(applicationModule: CommonsApplicationModule): Builder + + fun build(): CommonsApplicationComponent + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java deleted file mode 100644 index 3f9344184b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java +++ /dev/null @@ -1,314 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.content.Context; -import android.view.inputmethod.InputMethodManager; -import androidx.collection.LruCache; -import androidx.room.Room; -import androidx.room.migration.Migration; -import androidx.sqlite.db.SupportSQLiteDatabase; -import com.google.gson.Gson; -import dagger.Module; -import dagger.Provides; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.contributions.ContributionDao; -import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao; -import fr.free.nrw.commons.customselector.database.UploadedStatusDao; -import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader; -import fr.free.nrw.commons.data.DBOpenHelper; -import fr.free.nrw.commons.db.AppDatabase; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.nearby.PlaceDao; -import fr.free.nrw.commons.review.ReviewDao; -import fr.free.nrw.commons.settings.Prefs; -import fr.free.nrw.commons.upload.UploadController; -import fr.free.nrw.commons.upload.depicts.DepictsDao; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.wikidata.WikidataEditListener; -import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl; -import io.reactivex.Scheduler; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import javax.inject.Named; -import javax.inject.Singleton; - -/** - * The Dependency Provider class for Commons Android. - * - * Provides all sorts of ContentProviderClients used by the app - * along with the Liscences, AccountUtility, UploadController, Logged User, - * Location manager etc - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public class CommonsApplicationModule { - private Context applicationContext; - public static final String IO_THREAD="io_thread"; - public static final String MAIN_THREAD="main_thread"; - private AppDatabase appDatabase; - - static final Migration MIGRATION_1_2 = new Migration(1, 2) { - @Override - public void migrate(SupportSQLiteDatabase database) { - database.execSQL("ALTER TABLE contribution " - + " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0"); - } - }; - - public CommonsApplicationModule(Context applicationContext) { - this.applicationContext = applicationContext; - } - - /** - * Provides ImageFileLoader used to fetch device images. - * @param context - * @return - */ - @Provides - public ImageFileLoader providesImageFileLoader(Context context) { - return new ImageFileLoader(context); - } - - @Provides - public Context providesApplicationContext() { - return this.applicationContext; - } - - @Provides - public InputMethodManager provideInputMethodManager() { - return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE); - } - - @Provides - @Named("licenses") - public List provideLicenses(Context context) { - List licenseItems = new ArrayList<>(); - licenseItems.add(context.getString(R.string.license_name_cc0)); - licenseItems.add(context.getString(R.string.license_name_cc_by)); - licenseItems.add(context.getString(R.string.license_name_cc_by_sa)); - licenseItems.add(context.getString(R.string.license_name_cc_by_four)); - licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four)); - return licenseItems; - } - - @Provides - @Named("licenses_by_name") - public Map provideLicensesByName(Context context) { - Map byName = new HashMap<>(); - byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0); - byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3); - byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3); - byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4); - byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4); - return byName; - } - - /** - * Provides an instance of CategoryContentProviderClient i.e. the categories - * that are there in local storage - */ - @Provides - @Named("category") - public ContentProviderClient provideCategoryContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY); - } - - /** - * This method is used to provide instance of RecentSearchContentProviderClient - * which provides content of Recent Searches from database - * @param context - * @return returns RecentSearchContentProviderClient - */ - @Provides - @Named("recentsearch") - public ContentProviderClient provideRecentSearchContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY); - } - - @Provides - @Named("contribution") - public ContentProviderClient provideContributionContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY); - } - - @Provides - @Named("modification") - public ContentProviderClient provideModificationContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY); - } - - @Provides - @Named("bookmarks") - public ContentProviderClient provideBookmarkContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY); - } - - @Provides - @Named("bookmarksLocation") - public ContentProviderClient provideBookmarkLocationContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY); - } - - @Provides - @Named("bookmarksItem") - public ContentProviderClient provideBookmarkItemContentProviderClient(Context context) { - return context.getContentResolver().acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY); - } - - /** - * This method is used to provide instance of RecentLanguagesContentProvider - * which provides content of recent used languages from database - * @param context Context - * @return returns RecentLanguagesContentProvider - */ - @Provides - @Named("recent_languages") - public ContentProviderClient provideRecentLanguagesContentProviderClient(final Context context) { - return context.getContentResolver() - .acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY); - } - - /** - * Provides a Json store instance(JsonKvStore) which keeps - * the provided Gson in it's instance - * @param gson stored inside the store instance - */ - @Provides - @Named("default_preferences") - public JsonKvStore providesDefaultKvStore(Context context, Gson gson) { - String storeName = context.getPackageName() + "_preferences"; - return new JsonKvStore(context, storeName, gson); - } - - @Provides - public UploadController providesUploadController(SessionManager sessionManager, - @Named("default_preferences") JsonKvStore kvStore, - Context context, ContributionDao contributionDao) { - return new UploadController(sessionManager, context, kvStore); - } - - @Provides - @Singleton - public LocationServiceManager provideLocationServiceManager(Context context) { - return new LocationServiceManager(context); - } - - @Provides - @Singleton - public DBOpenHelper provideDBOpenHelper(Context context) { - return new DBOpenHelper(context); - } - - @Provides - @Singleton - @Named("thumbnail-cache") - public LruCache provideLruCache() { - return new LruCache<>(1024); - } - - @Provides - @Singleton - public WikidataEditListener provideWikidataEditListener() { - return new WikidataEditListenerImpl(); - } - - /** - * Provides app flavour. Can be used to alter flows in the app - * @return - */ - @Named("isBeta") - @Provides - @Singleton - public boolean provideIsBetaVariant() { - return ConfigUtils.isBetaFlavour(); - } - - /** - * Provide JavaRx IO scheduler which manages IO operations - * across various Threads - */ - @Named(IO_THREAD) - @Provides - public Scheduler providesIoThread(){ - return Schedulers.io(); - } - - @Named(MAIN_THREAD) - @Provides - public Scheduler providesMainThread() { - return AndroidSchedulers.mainThread(); - } - - @Named("username") - @Provides - public String provideLoggedInUsername(SessionManager sessionManager) { - return Objects.toString(sessionManager.getUserName(), ""); - } - - @Provides - @Singleton - public AppDatabase provideAppDataBase() { - appDatabase = Room.databaseBuilder(applicationContext, AppDatabase.class, "commons_room.db") - .addMigrations(MIGRATION_1_2) - .fallbackToDestructiveMigration() - .build(); - return appDatabase; - } - - @Provides - public ContributionDao providesContributionsDao(AppDatabase appDatabase) { - return appDatabase.contributionDao(); - } - - @Provides - public PlaceDao providesPlaceDao(AppDatabase appDatabase) { - return appDatabase.PlaceDao(); - } - - /** - * Get the reference of DepictsDao class. - */ - @Provides - public DepictsDao providesDepictDao(AppDatabase appDatabase) { - return appDatabase.DepictsDao(); - } - - /** - * Get the reference of UploadedStatus class. - */ - @Provides - public UploadedStatusDao providesUploadedStatusDao(AppDatabase appDatabase) { - return appDatabase.UploadedStatusDao(); - } - - /** - * Get the reference of NotForUploadStatus class. - */ - @Provides - public NotForUploadStatusDao providesNotForUploadStatusDao(AppDatabase appDatabase) { - return appDatabase.NotForUploadStatusDao(); - } - - /** - * Get the reference of ReviewDao class - */ - @Provides - public ReviewDao providesReviewDao(AppDatabase appDatabase){ - return appDatabase.ReviewDao(); - } - - @Provides - public ContentResolver providesContentResolver(Context context){ - return context.getContentResolver(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt new file mode 100644 index 0000000000..6f883769f2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.kt @@ -0,0 +1,239 @@ +package fr.free.nrw.commons.di + +import android.app.Activity +import android.content.ContentProviderClient +import android.content.ContentResolver +import android.content.Context +import android.view.inputmethod.InputMethodManager +import androidx.collection.LruCache +import androidx.room.Room.databaseBuilder +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.contributions.ContributionDao +import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao +import fr.free.nrw.commons.customselector.database.UploadedStatusDao +import fr.free.nrw.commons.customselector.ui.selector.ImageFileLoader +import fr.free.nrw.commons.data.DBOpenHelper +import fr.free.nrw.commons.db.AppDatabase +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.nearby.PlaceDao +import fr.free.nrw.commons.review.ReviewDao +import fr.free.nrw.commons.settings.Prefs +import fr.free.nrw.commons.upload.UploadController +import fr.free.nrw.commons.upload.depicts.DepictsDao +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.wikidata.WikidataEditListener +import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl +import io.reactivex.Scheduler +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import java.util.Objects +import javax.inject.Named +import javax.inject.Singleton + +/** + * The Dependency Provider class for Commons Android. + * Provides all sorts of ContentProviderClients used by the app + * along with the Liscences, AccountUtility, UploadController, Logged User, + * Location manager etc + */ +@Module +@Suppress("unused") +open class CommonsApplicationModule(private val applicationContext: Context) { + @Provides + fun providesImageFileLoader(context: Context): ImageFileLoader = + ImageFileLoader(context) + + @Provides + fun providesApplicationContext(): Context = + applicationContext + + @Provides + fun provideInputMethodManager(): InputMethodManager = + applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + + @Provides + @Named("licenses") + fun provideLicenses(context: Context): List = listOf( + context.getString(R.string.license_name_cc0), + context.getString(R.string.license_name_cc_by), + context.getString(R.string.license_name_cc_by_sa), + context.getString(R.string.license_name_cc_by_four), + context.getString(R.string.license_name_cc_by_sa_four) + ) + + @Provides + @Named("licenses_by_name") + fun provideLicensesByName(context: Context): Map = mapOf( + context.getString(R.string.license_name_cc0) to Prefs.Licenses.CC0, + context.getString(R.string.license_name_cc_by) to Prefs.Licenses.CC_BY_3, + context.getString(R.string.license_name_cc_by_sa) to Prefs.Licenses.CC_BY_SA_3, + context.getString(R.string.license_name_cc_by_four) to Prefs.Licenses.CC_BY_4, + context.getString(R.string.license_name_cc_by_sa_four) to Prefs.Licenses.CC_BY_SA_4 + ) + + /** + * Provides an instance of CategoryContentProviderClient i.e. the categories + * that are there in local storage + */ + @Provides + @Named("category") + open fun provideCategoryContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY) + + @Provides + @Named("recentsearch") + fun provideRecentSearchContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_SEARCH_AUTHORITY) + + @Provides + @Named("contribution") + open fun provideContributionContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.CONTRIBUTION_AUTHORITY) + + @Provides + @Named("modification") + open fun provideModificationContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.MODIFICATION_AUTHORITY) + + @Provides + @Named("bookmarks") + fun provideBookmarkContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_AUTHORITY) + + @Provides + @Named("bookmarksLocation") + fun provideBookmarkLocationContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_LOCATIONS_AUTHORITY) + + @Provides + @Named("bookmarksItem") + fun provideBookmarkItemContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.BOOKMARK_ITEMS_AUTHORITY) + + /** + * This method is used to provide instance of RecentLanguagesContentProvider + * which provides content of recent used languages from database + * @param context Context + * @return returns RecentLanguagesContentProvider + */ + @Provides + @Named("recent_languages") + fun provideRecentLanguagesContentProviderClient(context: Context): ContentProviderClient? = + context.contentResolver.acquireContentProviderClient(BuildConfig.RECENT_LANGUAGE_AUTHORITY) + + /** + * Provides a Json store instance(JsonKvStore) which keeps + * the provided Gson in it's instance + * @param gson stored inside the store instance + */ + @Provides + @Named("default_preferences") + open fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore = + JsonKvStore(context, "${context.packageName}_preferences", gson) + + @Provides + fun providesUploadController( + sessionManager: SessionManager, + @Named("default_preferences") kvStore: JsonKvStore, + context: Context + ): UploadController = UploadController(sessionManager, context, kvStore) + + @Provides + @Singleton + open fun provideLocationServiceManager(context: Context): LocationServiceManager = + LocationServiceManager(context) + + @Provides + @Singleton + open fun provideDBOpenHelper(context: Context): DBOpenHelper = + DBOpenHelper(context) + + @Provides + @Singleton + @Named("thumbnail-cache") + open fun provideLruCache(): LruCache = + LruCache(1024) + + @Provides + @Singleton + fun provideWikidataEditListener(): WikidataEditListener = + WikidataEditListenerImpl() + + @Named("isBeta") + @Provides + @Singleton + fun provideIsBetaVariant(): Boolean = + isBetaFlavour + + @Named(IO_THREAD) + @Provides + fun providesIoThread(): Scheduler = + Schedulers.io() + + @Named(MAIN_THREAD) + @Provides + fun providesMainThread(): Scheduler = + AndroidSchedulers.mainThread() + + @Named("username") + @Provides + fun provideLoggedInUsername(sessionManager: SessionManager): String = + Objects.toString(sessionManager.userName, "") + + @Provides + @Singleton + fun provideAppDataBase(): AppDatabase = databaseBuilder( + applicationContext, + AppDatabase::class.java, + "commons_room.db" + ).addMigrations(MIGRATION_1_2).fallbackToDestructiveMigration().build() + + @Provides + fun providesContributionsDao(appDatabase: AppDatabase): ContributionDao = + appDatabase.contributionDao() + + @Provides + fun providesPlaceDao(appDatabase: AppDatabase): PlaceDao = + appDatabase.PlaceDao() + + @Provides + fun providesDepictDao(appDatabase: AppDatabase): DepictsDao = + appDatabase.DepictsDao() + + @Provides + fun providesUploadedStatusDao(appDatabase: AppDatabase): UploadedStatusDao = + appDatabase.UploadedStatusDao() + + @Provides + fun providesNotForUploadStatusDao(appDatabase: AppDatabase): NotForUploadStatusDao = + appDatabase.NotForUploadStatusDao() + + @Provides + fun providesReviewDao(appDatabase: AppDatabase): ReviewDao = + appDatabase.ReviewDao() + + @Provides + fun providesContentResolver(context: Context): ContentResolver = + context.contentResolver + + companion object { + const val IO_THREAD: String = "io_thread" + const val MAIN_THREAD: String = "main_thread" + + val MIGRATION_1_2: Migration = object : Migration(1, 2) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL( + "ALTER TABLE contribution " + " ADD COLUMN hasInvalidLocation INTEGER NOT NULL DEFAULT 0" + ) + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java deleted file mode 100644 index 003b3649cf..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.java +++ /dev/null @@ -1,48 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.os.Bundle; - -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.fragment.app.Fragment; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.support.HasSupportFragmentInjector; - -public abstract class CommonsDaggerAppCompatActivity extends AppCompatActivity implements HasSupportFragmentInjector { - - @Inject - DispatchingAndroidInjector supportFragmentInjector; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - inject(); - super.onCreate(savedInstanceState); - } - - @Override - public AndroidInjector supportFragmentInjector() { - return supportFragmentInjector; - } - - /** - * when this Activity is created it injects an instance of this class inside - * activityInjector method of ApplicationlessInjection - */ - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); - - AndroidInjector activityInjector = injection.activityInjector(); - - if (activityInjector == null) { - throw new NullPointerException("ApplicationlessInjection.activityInjector() returned null"); - } - - activityInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt new file mode 100644 index 0000000000..fe9c7adee8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerAppCompatActivity.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.di + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.support.HasSupportFragmentInjector +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance +import javax.inject.Inject + +abstract class CommonsDaggerAppCompatActivity : AppCompatActivity(), HasSupportFragmentInjector { + @Inject @JvmField + var supportFragmentInjector: DispatchingAndroidInjector? = null + + override fun onCreate(savedInstanceState: Bundle?) { + inject() + super.onCreate(savedInstanceState) + } + + override fun supportFragmentInjector(): AndroidInjector { + return supportFragmentInjector!! + } + + /** + * when this Activity is created it injects an instance of this class inside + * activityInjector method of ApplicationlessInjection + */ + private fun inject() { + val injection = getInstance(applicationContext) + + val activityInjector = injection.activityInjector() + ?: throw NullPointerException("ApplicationlessInjection.activityInjector() returned null") + + activityInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java deleted file mode 100644 index 0b89003b5e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.java +++ /dev/null @@ -1,35 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; - -import dagger.android.AndroidInjector; - -/** - * Receives broadcast then injects it's instance to the broadcastReceiverInjector method of - * ApplicationlessInjection class - */ -public abstract class CommonsDaggerBroadcastReceiver extends BroadcastReceiver { - - public CommonsDaggerBroadcastReceiver() { - super(); - } - - @Override - public void onReceive(Context context, Intent intent) { - inject(context); - } - - private void inject(Context context) { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(context.getApplicationContext()); - - AndroidInjector serviceInjector = injection.broadcastReceiverInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null"); - } - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt new file mode 100644 index 0000000000..4df25889f0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerBroadcastReceiver.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.di + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +/** + * Receives broadcast then injects it's instance to the broadcastReceiverInjector method of + * ApplicationlessInjection class + */ +abstract class CommonsDaggerBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + inject(context) + } + + private fun inject(context: Context) { + val injection = getInstance(context.applicationContext) + + val serviceInjector = injection.broadcastReceiverInjector() + ?: throw NullPointerException("ApplicationlessInjection.broadcastReceiverInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java deleted file mode 100644 index 06adee489d..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.content.ContentProvider; - -import dagger.android.AndroidInjector; - - -public abstract class CommonsDaggerContentProvider extends ContentProvider { - - public CommonsDaggerContentProvider() { - super(); - } - - @Override - public boolean onCreate() { - inject(); - return true; - } - - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getContext()); - - AndroidInjector serviceInjector = injection.contentProviderInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null"); - } - - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt new file mode 100644 index 0000000000..c1bda689c4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerContentProvider.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.di + +import android.content.ContentProvider +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +abstract class CommonsDaggerContentProvider : ContentProvider() { + override fun onCreate(): Boolean { + inject() + return true + } + + private fun inject() { + val injection = getInstance(context!!) + + val serviceInjector = injection.contentProviderInjector() + ?: throw NullPointerException("ApplicationlessInjection.contentProviderInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java deleted file mode 100644 index 41f661db45..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.IntentService; -import android.app.Service; - -import dagger.android.AndroidInjector; - -public abstract class CommonsDaggerIntentService extends IntentService { - - public CommonsDaggerIntentService(String name) { - super(name); - } - - @Override - public void onCreate() { - inject(); - super.onCreate(); - } - - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); - - AndroidInjector serviceInjector = injection.serviceInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); - } - - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt new file mode 100644 index 0000000000..4aae35f0b2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerIntentService.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.di + +import android.app.IntentService +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +abstract class CommonsDaggerIntentService(name: String?) : IntentService(name) { + override fun onCreate() { + inject() + super.onCreate() + } + + private fun inject() { + val injection = getInstance(applicationContext) + + val serviceInjector = injection.serviceInjector() + ?: throw NullPointerException("ApplicationlessInjection.serviceInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java deleted file mode 100644 index 0d045d2ceb..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.java +++ /dev/null @@ -1,31 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Service; - -import dagger.android.AndroidInjector; - -public abstract class CommonsDaggerService extends Service { - - public CommonsDaggerService() { - super(); - } - - @Override - public void onCreate() { - inject(); - super.onCreate(); - } - - private void inject() { - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(getApplicationContext()); - - AndroidInjector serviceInjector = injection.serviceInjector(); - - if (serviceInjector == null) { - throw new NullPointerException("ApplicationlessInjection.serviceInjector() returned null"); - } - - serviceInjector.inject(this); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt new file mode 100644 index 0000000000..3a67e0d2a6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerService.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.di + +import android.app.Service +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance + +abstract class CommonsDaggerService : Service() { + override fun onCreate() { + inject() + super.onCreate() + } + + private fun inject() { + val injection = getInstance(applicationContext) + + val serviceInjector = injection.serviceInjector() + ?: throw NullPointerException("ApplicationlessInjection.serviceInjector() returned null") + + serviceInjector.inject(this) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java deleted file mode 100644 index f5ef2dd288..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.java +++ /dev/null @@ -1,75 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.app.Activity; -import android.content.Context; - -import androidx.fragment.app.Fragment; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.support.HasSupportFragmentInjector; -import io.reactivex.disposables.CompositeDisposable; - -public abstract class CommonsDaggerSupportFragment extends Fragment implements HasSupportFragmentInjector { - - @Inject - DispatchingAndroidInjector childFragmentInjector; - - protected CompositeDisposable compositeDisposable = new CompositeDisposable(); - - @Override - public void onAttach(Context context) { - inject(); - super.onAttach(context); - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - } - - @Override - public AndroidInjector supportFragmentInjector() { - return childFragmentInjector; - } - - - public void inject() { - HasSupportFragmentInjector hasSupportFragmentInjector = findHasFragmentInjector(); - - AndroidInjector fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector(); - - if (fragmentInjector == null) { - throw new NullPointerException(String.format("%s.supportFragmentInjector() returned null", hasSupportFragmentInjector.getClass().getCanonicalName())); - } - - fragmentInjector.inject(this); - } - - private HasSupportFragmentInjector findHasFragmentInjector() { - Fragment parentFragment = this; - - while ((parentFragment = parentFragment.getParentFragment()) != null) { - if (parentFragment instanceof HasSupportFragmentInjector) { - return (HasSupportFragmentInjector) parentFragment; - } - } - - Activity activity = getActivity(); - - if (activity instanceof HasSupportFragmentInjector) { - return (HasSupportFragmentInjector) activity; - } - - ApplicationlessInjection injection = ApplicationlessInjection.getInstance(activity.getApplicationContext()); - if (injection != null) { - return injection; - } - - throw new IllegalArgumentException(String.format("No injector was found for %s", getClass().getCanonicalName())); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt new file mode 100644 index 0000000000..8204d44156 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsDaggerSupportFragment.kt @@ -0,0 +1,66 @@ +package fr.free.nrw.commons.di + +import android.app.Activity +import android.content.Context +import androidx.fragment.app.Fragment +import dagger.android.AndroidInjector +import dagger.android.DispatchingAndroidInjector +import dagger.android.support.HasSupportFragmentInjector +import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance +import io.reactivex.disposables.CompositeDisposable +import javax.inject.Inject + +abstract class CommonsDaggerSupportFragment : Fragment(), HasSupportFragmentInjector { + + @Inject @JvmField + var childFragmentInjector: DispatchingAndroidInjector? = null + + @JvmField + protected var compositeDisposable: CompositeDisposable = CompositeDisposable() + + override fun onAttach(context: Context) { + inject() + super.onAttach(context) + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + } + + override fun supportFragmentInjector(): AndroidInjector = + childFragmentInjector!! + + + fun inject() { + val hasSupportFragmentInjector = findHasFragmentInjector() + + val fragmentInjector = hasSupportFragmentInjector.supportFragmentInjector() + ?: throw NullPointerException( + String.format( + "%s.supportFragmentInjector() returned null", + hasSupportFragmentInjector.javaClass.canonicalName + ) + ) + + fragmentInjector.inject(this) + } + + private fun findHasFragmentInjector(): HasSupportFragmentInjector { + var parentFragment: Fragment? = this + + while ((parentFragment!!.parentFragment.also { parentFragment = it }) != null) { + if (parentFragment is HasSupportFragmentInjector) { + return parentFragment as HasSupportFragmentInjector + } + } + + val activity: Activity = requireActivity() + + if (activity is HasSupportFragmentInjector) { + return activity + } + + return getInstance(activity.applicationContext) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java deleted file mode 100644 index aca6a2bf9f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java +++ /dev/null @@ -1,38 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider; -import fr.free.nrw.commons.category.CategoryContentProvider; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider; -import fr.free.nrw.commons.recentlanguages.RecentLanguagesContentProvider; - -/** - * This Class Represents the Module for dependency injection (using dagger) - * so, if a developer needs to add a new ContentProvider to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({ "WeakerAccess", "unused" }) -public abstract class ContentProviderBuilderModule { - - @ContributesAndroidInjector - abstract CategoryContentProvider bindCategoryContentProvider(); - - @ContributesAndroidInjector - abstract RecentSearchesContentProvider bindRecentSearchesContentProvider(); - - @ContributesAndroidInjector - abstract BookmarkPicturesContentProvider bindBookmarkContentProvider(); - - @ContributesAndroidInjector - abstract BookmarkLocationsContentProvider bindBookmarkLocationContentProvider(); - - @ContributesAndroidInjector - abstract BookmarkItemsContentProvider bindBookmarkItemContentProvider(); - - @ContributesAndroidInjector - abstract RecentLanguagesContentProvider bindRecentLanguagesContentProvider(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt new file mode 100644 index 0000000000..1882f77a97 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.kt @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider +import fr.free.nrw.commons.category.CategoryContentProvider +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider +import fr.free.nrw.commons.recentlanguages.RecentLanguagesContentProvider + +/** + * This Class Represents the Module for dependency injection (using dagger) + * so, if a developer needs to add a new ContentProvider to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class ContentProviderBuilderModule { + @ContributesAndroidInjector + abstract fun bindCategoryContentProvider(): CategoryContentProvider + + @ContributesAndroidInjector + abstract fun bindRecentSearchesContentProvider(): RecentSearchesContentProvider + + @ContributesAndroidInjector + abstract fun bindBookmarkContentProvider(): BookmarkPicturesContentProvider + + @ContributesAndroidInjector + abstract fun bindBookmarkLocationContentProvider(): BookmarkLocationsContentProvider + + @ContributesAndroidInjector + abstract fun bindBookmarkItemContentProvider(): BookmarkItemsContentProvider + + @ContributesAndroidInjector + abstract fun bindRecentLanguagesContentProvider(): RecentLanguagesContentProvider +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java deleted file mode 100644 index 698ca1500f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ /dev/null @@ -1,166 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.bookmarks.BookmarkFragment; -import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment; -import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment; -import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment; -import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment; -import fr.free.nrw.commons.contributions.ContributionsFragment; -import fr.free.nrw.commons.contributions.ContributionsListFragment; -import fr.free.nrw.commons.customselector.ui.selector.FolderFragment; -import fr.free.nrw.commons.customselector.ui.selector.ImageFragment; -import fr.free.nrw.commons.explore.ExploreFragment; -import fr.free.nrw.commons.explore.ExploreListRootFragment; -import fr.free.nrw.commons.explore.ExploreMapRootFragment; -import fr.free.nrw.commons.explore.map.ExploreMapFragment; -import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment; -import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment; -import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment; -import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment; -import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment; -import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment; -import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment; -import fr.free.nrw.commons.explore.media.SearchMediaFragment; -import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; -import fr.free.nrw.commons.media.MediaDetailFragment; -import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import fr.free.nrw.commons.navtab.MoreBottomSheetFragment; -import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment; -import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment; -import fr.free.nrw.commons.profile.achievements.AchievementsFragment; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment; -import fr.free.nrw.commons.review.ReviewImageFragment; -import fr.free.nrw.commons.settings.SettingsFragment; -import fr.free.nrw.commons.upload.FailedUploadsFragment; -import fr.free.nrw.commons.upload.PendingUploadsFragment; -import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment; -import fr.free.nrw.commons.upload.depicts.DepictsFragment; -import fr.free.nrw.commons.upload.license.MediaLicenseFragment; -import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment; - -/** - * This Class Represents the Module for dependency injection (using dagger) - * so, if a developer needs to add a new Fragment to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class FragmentBuilderModule { - - @ContributesAndroidInjector - abstract ContributionsListFragment bindContributionsListFragment(); - - @ContributesAndroidInjector - abstract MediaDetailFragment bindMediaDetailFragment(); - - @ContributesAndroidInjector - abstract FolderFragment bindFolderFragment(); - - @ContributesAndroidInjector - abstract ImageFragment bindImageFragment(); - - @ContributesAndroidInjector - abstract MediaDetailPagerFragment bindMediaDetailPagerFragment(); - - @ContributesAndroidInjector - abstract SettingsFragment bindSettingsFragment(); - - @ContributesAndroidInjector - abstract DepictedImagesFragment bindDepictedImagesFragment(); - - @ContributesAndroidInjector - abstract SearchMediaFragment bindBrowseImagesListFragment(); - - @ContributesAndroidInjector - abstract SearchCategoryFragment bindSearchCategoryListFragment(); - - @ContributesAndroidInjector - abstract SearchDepictionsFragment bindSearchDepictionListFragment(); - - @ContributesAndroidInjector - abstract RecentSearchesFragment bindRecentSearchesFragment(); - - @ContributesAndroidInjector - abstract ContributionsFragment bindContributionsFragment(); - - @ContributesAndroidInjector(modules = NearbyParentFragmentModule.class) - abstract NearbyParentFragment bindNearbyParentFragment(); - - @ContributesAndroidInjector - abstract BookmarkPicturesFragment bindBookmarkPictureListFragment(); - - @ContributesAndroidInjector(modules = BookmarkLocationsFragmentModule.class) - abstract BookmarkLocationsFragment bindBookmarkLocationListFragment(); - - @ContributesAndroidInjector(modules = BookmarkItemsFragmentModule.class) - abstract BookmarkItemsFragment bindBookmarkItemListFragment(); - - @ContributesAndroidInjector - abstract ReviewImageFragment bindReviewOutOfContextFragment(); - - @ContributesAndroidInjector - abstract UploadMediaDetailFragment bindUploadMediaDetailFragment(); - - @ContributesAndroidInjector - abstract UploadCategoriesFragment bindUploadCategoriesFragment(); - - @ContributesAndroidInjector - abstract DepictsFragment bindDepictsFragment(); - - @ContributesAndroidInjector - abstract MediaLicenseFragment bindMediaLicenseFragment(); - - @ContributesAndroidInjector - abstract ParentDepictionsFragment bindParentDepictionsFragment(); - - @ContributesAndroidInjector - abstract ChildDepictionsFragment bindChildDepictionsFragment(); - - @ContributesAndroidInjector - abstract CategoriesMediaFragment bindCategoriesMediaFragment(); - - @ContributesAndroidInjector - abstract SubCategoriesFragment bindSubCategoriesFragment(); - - @ContributesAndroidInjector - abstract ParentCategoriesFragment bindParentCategoriesFragment(); - - @ContributesAndroidInjector - abstract ExploreFragment bindExploreFragmentFragment(); - - @ContributesAndroidInjector - abstract ExploreListRootFragment bindExploreFeaturedRootFragment(); - - @ContributesAndroidInjector(modules = ExploreMapFragmentModule.class) - abstract ExploreMapFragment bindExploreNearbyUploadsFragment(); - - @ContributesAndroidInjector - abstract ExploreMapRootFragment bindExploreNearbyUploadsRootFragment(); - - @ContributesAndroidInjector - abstract BookmarkListRootFragment bindBookmarkListRootFragment(); - - @ContributesAndroidInjector - abstract BookmarkFragment bindBookmarkFragmentFragment(); - - @ContributesAndroidInjector - abstract MoreBottomSheetFragment bindMoreBottomSheetFragment(); - - @ContributesAndroidInjector - abstract MoreBottomSheetLoggedOutFragment bindMoreBottomSheetLoggedOutFragment(); - - @ContributesAndroidInjector - abstract AchievementsFragment bindAchievementsFragment(); - - @ContributesAndroidInjector - abstract LeaderboardFragment bindLeaderboardFragment(); - - @ContributesAndroidInjector - abstract PendingUploadsFragment bindPendingUploadsFragment(); - - @ContributesAndroidInjector - abstract FailedUploadsFragment bindFailedUploadsFragment(); -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt new file mode 100644 index 0000000000..bfdb90181a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.kt @@ -0,0 +1,165 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.bookmarks.BookmarkFragment +import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment +import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment +import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment +import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment +import fr.free.nrw.commons.contributions.ContributionsFragment +import fr.free.nrw.commons.contributions.ContributionsListFragment +import fr.free.nrw.commons.customselector.ui.selector.FolderFragment +import fr.free.nrw.commons.customselector.ui.selector.ImageFragment +import fr.free.nrw.commons.explore.ExploreFragment +import fr.free.nrw.commons.explore.ExploreListRootFragment +import fr.free.nrw.commons.explore.ExploreMapRootFragment +import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment +import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment +import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment +import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment +import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment +import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment +import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment +import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment +import fr.free.nrw.commons.explore.map.ExploreMapFragment +import fr.free.nrw.commons.explore.media.SearchMediaFragment +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment +import fr.free.nrw.commons.media.MediaDetailFragment +import fr.free.nrw.commons.media.MediaDetailPagerFragment +import fr.free.nrw.commons.navtab.MoreBottomSheetFragment +import fr.free.nrw.commons.navtab.MoreBottomSheetLoggedOutFragment +import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment +import fr.free.nrw.commons.profile.achievements.AchievementsFragment +import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment +import fr.free.nrw.commons.review.ReviewImageFragment +import fr.free.nrw.commons.settings.SettingsFragment +import fr.free.nrw.commons.upload.FailedUploadsFragment +import fr.free.nrw.commons.upload.PendingUploadsFragment +import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment +import fr.free.nrw.commons.upload.depicts.DepictsFragment +import fr.free.nrw.commons.upload.license.MediaLicenseFragment +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment + +/** + * This Class Represents the Module for dependency injection (using dagger) + * so, if a developer needs to add a new Fragment to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class FragmentBuilderModule { + @ContributesAndroidInjector + abstract fun bindContributionsListFragment(): ContributionsListFragment + + @ContributesAndroidInjector + abstract fun bindMediaDetailFragment(): MediaDetailFragment + + @ContributesAndroidInjector + abstract fun bindFolderFragment(): FolderFragment + + @ContributesAndroidInjector + abstract fun bindImageFragment(): ImageFragment + + @ContributesAndroidInjector + abstract fun bindMediaDetailPagerFragment(): MediaDetailPagerFragment + + @ContributesAndroidInjector + abstract fun bindSettingsFragment(): SettingsFragment + + @ContributesAndroidInjector + abstract fun bindDepictedImagesFragment(): DepictedImagesFragment + + @ContributesAndroidInjector + abstract fun bindBrowseImagesListFragment(): SearchMediaFragment + + @ContributesAndroidInjector + abstract fun bindSearchCategoryListFragment(): SearchCategoryFragment + + @ContributesAndroidInjector + abstract fun bindSearchDepictionListFragment(): SearchDepictionsFragment + + @ContributesAndroidInjector + abstract fun bindRecentSearchesFragment(): RecentSearchesFragment + + @ContributesAndroidInjector + abstract fun bindContributionsFragment(): ContributionsFragment + + @ContributesAndroidInjector(modules = [NearbyParentFragmentModule::class]) + abstract fun bindNearbyParentFragment(): NearbyParentFragment + + @ContributesAndroidInjector + abstract fun bindBookmarkPictureListFragment(): BookmarkPicturesFragment + + @ContributesAndroidInjector(modules = [BookmarkLocationsFragmentModule::class]) + abstract fun bindBookmarkLocationListFragment(): BookmarkLocationsFragment + + @ContributesAndroidInjector(modules = [BookmarkItemsFragmentModule::class]) + abstract fun bindBookmarkItemListFragment(): BookmarkItemsFragment + + @ContributesAndroidInjector + abstract fun bindReviewOutOfContextFragment(): ReviewImageFragment + + @ContributesAndroidInjector + abstract fun bindUploadMediaDetailFragment(): UploadMediaDetailFragment + + @ContributesAndroidInjector + abstract fun bindUploadCategoriesFragment(): UploadCategoriesFragment + + @ContributesAndroidInjector + abstract fun bindDepictsFragment(): DepictsFragment + + @ContributesAndroidInjector + abstract fun bindMediaLicenseFragment(): MediaLicenseFragment + + @ContributesAndroidInjector + abstract fun bindParentDepictionsFragment(): ParentDepictionsFragment + + @ContributesAndroidInjector + abstract fun bindChildDepictionsFragment(): ChildDepictionsFragment + + @ContributesAndroidInjector + abstract fun bindCategoriesMediaFragment(): CategoriesMediaFragment + + @ContributesAndroidInjector + abstract fun bindSubCategoriesFragment(): SubCategoriesFragment + + @ContributesAndroidInjector + abstract fun bindParentCategoriesFragment(): ParentCategoriesFragment + + @ContributesAndroidInjector + abstract fun bindExploreFragmentFragment(): ExploreFragment + + @ContributesAndroidInjector + abstract fun bindExploreFeaturedRootFragment(): ExploreListRootFragment + + @ContributesAndroidInjector(modules = [ExploreMapFragmentModule::class]) + abstract fun bindExploreNearbyUploadsFragment(): ExploreMapFragment + + @ContributesAndroidInjector + abstract fun bindExploreNearbyUploadsRootFragment(): ExploreMapRootFragment + + @ContributesAndroidInjector + abstract fun bindBookmarkListRootFragment(): BookmarkListRootFragment + + @ContributesAndroidInjector + abstract fun bindBookmarkFragmentFragment(): BookmarkFragment + + @ContributesAndroidInjector + abstract fun bindMoreBottomSheetFragment(): MoreBottomSheetFragment + + @ContributesAndroidInjector + abstract fun bindMoreBottomSheetLoggedOutFragment(): MoreBottomSheetLoggedOutFragment + + @ContributesAndroidInjector + abstract fun bindAchievementsFragment(): AchievementsFragment + + @ContributesAndroidInjector + abstract fun bindLeaderboardFragment(): LeaderboardFragment + + @ContributesAndroidInjector + abstract fun bindPendingUploadsFragment(): PendingUploadsFragment + + @ContributesAndroidInjector + abstract fun bindFailedUploadsFragment(): FailedUploadsFragment +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java deleted file mode 100644 index 6aef8d3231..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java +++ /dev/null @@ -1,350 +0,0 @@ -package fr.free.nrw.commons.di; - -import android.content.Context; -import androidx.annotation.NonNull; -import com.google.gson.Gson; -import dagger.Module; -import dagger.Provides; -import fr.free.nrw.commons.BetaConstants; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.OkHttpConnectionFactory; -import fr.free.nrw.commons.actions.PageEditClient; -import fr.free.nrw.commons.actions.PageEditInterface; -import fr.free.nrw.commons.actions.ThanksInterface; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; -import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface; -import fr.free.nrw.commons.auth.csrf.LogoutClient; -import fr.free.nrw.commons.auth.login.LoginClient; -import fr.free.nrw.commons.auth.login.LoginInterface; -import fr.free.nrw.commons.category.CategoryInterface; -import fr.free.nrw.commons.explore.depictions.DepictsClient; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.media.MediaDetailInterface; -import fr.free.nrw.commons.media.MediaInterface; -import fr.free.nrw.commons.media.PageMediaInterface; -import fr.free.nrw.commons.media.WikidataMediaInterface; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.mwapi.UserInterface; -import fr.free.nrw.commons.notification.NotificationInterface; -import fr.free.nrw.commons.review.ReviewInterface; -import fr.free.nrw.commons.upload.UploadInterface; -import fr.free.nrw.commons.upload.WikiBaseInterface; -import fr.free.nrw.commons.upload.depicts.DepictsInterface; -import fr.free.nrw.commons.wikidata.CommonsServiceFactory; -import fr.free.nrw.commons.wikidata.WikidataInterface; -import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar; -import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage; -import java.io.File; -import java.util.Locale; -import java.util.concurrent.TimeUnit; -import javax.inject.Named; -import javax.inject.Singleton; -import okhttp3.Cache; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.logging.HttpLoggingInterceptor; -import okhttp3.logging.HttpLoggingInterceptor.Level; -import fr.free.nrw.commons.wikidata.model.WikiSite; -import fr.free.nrw.commons.wikidata.GsonUtil; -import timber.log.Timber; - -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public class NetworkingModule { - private static final String WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql"; - private static final String TOOLS_FORGE_URL = "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app"; - - public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; - - private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite"; - private static final String NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite"; - - public static final String NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE = "language-wikipedia-wikisite"; - - public static final String NAMED_COMMONS_CSRF = "commons-csrf"; - public static final String NAMED_WIKI_CSRF = "wiki-csrf"; - - @Provides - @Singleton - public OkHttpClient provideOkHttpClient(Context context, - HttpLoggingInterceptor httpLoggingInterceptor) { - File dir = new File(context.getCacheDir(), "okHttpCache"); - return new OkHttpClient.Builder() - .connectTimeout(120, TimeUnit.SECONDS) - .writeTimeout(120, TimeUnit.SECONDS) - .addInterceptor(httpLoggingInterceptor) - .readTimeout(120, TimeUnit.SECONDS) - .cache(new Cache(dir, OK_HTTP_CACHE_SIZE)) - .build(); - } - - @Provides - @Singleton - public CommonsServiceFactory serviceFactory(CommonsCookieJar cookieJar) { - return new CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar)); - } - - @Provides - @Singleton - public HttpLoggingInterceptor provideHttpLoggingInterceptor() { - HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(message -> { - Timber.tag("OkHttp").v(message); - }); - httpLoggingInterceptor.setLevel(BuildConfig.DEBUG ? Level.BODY: Level.BASIC); - return httpLoggingInterceptor; - } - - @Provides - @Singleton - public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient, - DepictsClient depictsClient, - @Named("tools_forge") HttpUrl toolsForgeUrl, - @Named("default_preferences") JsonKvStore defaultKvStore, - Gson gson) { - return new OkHttpJsonApiClient(okHttpClient, - depictsClient, - toolsForgeUrl, - WIKIDATA_SPARQL_QUERY_URL, - BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, - gson); - } - - @Provides - @Singleton - public CommonsCookieStorage provideCookieStorage( - @Named("default_preferences") JsonKvStore preferences) { - CommonsCookieStorage cookieStorage = new CommonsCookieStorage(preferences); - cookieStorage.load(); - return cookieStorage; - } - - @Provides - @Singleton - public CommonsCookieJar provideCookieJar(CommonsCookieStorage storage) { - return new CommonsCookieJar(storage); - } - - @Named(NAMED_COMMONS_CSRF) - @Provides - @Singleton - public CsrfTokenClient provideCommonsCsrfTokenClient(SessionManager sessionManager, - @Named("commons-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) { - return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient); - } - - /** - * Provides a singleton instance of CsrfTokenClient for Wikidata. - * - * @param sessionManager The session manager to manage user sessions. - * @param tokenInterface The interface for obtaining CSRF tokens. - * @param loginClient The client for handling login operations. - * @param logoutClient The client for handling logout operations. - * @return A singleton instance of CsrfTokenClient. - */ - @Named(NAMED_WIKI_CSRF) - @Provides - @Singleton - public CsrfTokenClient provideWikiCsrfTokenClient(SessionManager sessionManager, - @Named("wikidata-csrf-interface") CsrfTokenInterface tokenInterface, LoginClient loginClient, LogoutClient logoutClient) { - return new CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient); - } - - /** - * Provides a singleton instance of CsrfTokenInterface for Wikidata. - * - * @param serviceFactory The factory used to create service interfaces. - * @return A singleton instance of CsrfTokenInterface for Wikidata. - */ - @Named("wikidata-csrf-interface") - @Provides - @Singleton - public CsrfTokenInterface provideWikidataCsrfTokenInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, CsrfTokenInterface.class); - } - - @Named("commons-csrf-interface") - @Provides - @Singleton - public CsrfTokenInterface provideCsrfTokenInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, CsrfTokenInterface.class); - } - - @Provides - @Singleton - public LoginInterface provideLoginInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, LoginInterface.class); - } - - @Provides - @Singleton - public LoginClient provideLoginClient(LoginInterface loginInterface) { - return new LoginClient(loginInterface); - } - - @Provides - @Named("wikimedia_api_host") - @NonNull - @SuppressWarnings("ConstantConditions") - public String provideMwApiUrl() { - return BuildConfig.WIKIMEDIA_API_HOST; - } - - @Provides - @Named("tools_forge") - @NonNull - @SuppressWarnings("ConstantConditions") - public HttpUrl provideToolsForgeUrl() { - return HttpUrl.parse(TOOLS_FORGE_URL); - } - - @Provides - @Singleton - @Named(NAMED_WIKI_DATA_WIKI_SITE) - public WikiSite provideWikidataWikiSite() { - return new WikiSite(BuildConfig.WIKIDATA_URL); - } - - - /** - * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. - * @return returns a singleton Gson instance - */ - @Provides - @Singleton - public Gson provideGson() { - return GsonUtil.getDefaultGson(); - } - - @Provides - @Singleton - public ReviewInterface provideReviewInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, ReviewInterface.class); - } - - @Provides - @Singleton - public DepictsInterface provideDepictsInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, DepictsInterface.class); - } - - @Provides - @Singleton - public WikiBaseInterface provideWikiBaseInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, WikiBaseInterface.class); - } - - @Provides - @Singleton - public UploadInterface provideUploadInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, UploadInterface.class); - } - - @Named("commons-page-edit-service") - @Provides - @Singleton - public PageEditInterface providePageEditService(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, PageEditInterface.class); - } - - @Named("wikidata-page-edit-service") - @Provides - @Singleton - public PageEditInterface provideWikiDataPageEditService(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, PageEditInterface.class); - } - - @Named("commons-page-edit") - @Provides - @Singleton - public PageEditClient provideCommonsPageEditClient(@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient, - @Named("commons-page-edit-service") PageEditInterface pageEditInterface) { - return new PageEditClient(csrfTokenClient, pageEditInterface); - } - - /** - * Provides a singleton instance of PageEditClient for Wikidata. - * - * @param csrfTokenClient The client used to manage CSRF tokens. - * @param pageEditInterface The interface for page edit operations. - * @return A singleton instance of PageEditClient for Wikidata. - */ - @Named("wikidata-page-edit") - @Provides - @Singleton - public PageEditClient provideWikidataPageEditClient(@Named(NAMED_WIKI_CSRF) CsrfTokenClient csrfTokenClient, - @Named("wikidata-page-edit-service") PageEditInterface pageEditInterface) { - return new PageEditClient(csrfTokenClient, pageEditInterface); - } - - @Provides - @Singleton - public MediaInterface provideMediaInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, MediaInterface.class); - } - - /** - * Add provider for WikidataMediaInterface - * It creates a retrofit service for the commons wiki site - * @param commonsWikiSite commonsWikiSite - * @return WikidataMediaInterface - */ - @Provides - @Singleton - public WikidataMediaInterface provideWikidataMediaInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BetaConstants.COMMONS_URL, WikidataMediaInterface.class); - } - - @Provides - @Singleton - public MediaDetailInterface providesMediaDetailInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, MediaDetailInterface.class); - } - - @Provides - @Singleton - public CategoryInterface provideCategoryInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, CategoryInterface.class); - } - - @Provides - @Singleton - public ThanksInterface provideThanksInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, ThanksInterface.class); - } - - @Provides - @Singleton - public NotificationInterface provideNotificationInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, NotificationInterface.class); - } - - @Provides - @Singleton - public UserInterface provideUserInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.COMMONS_URL, UserInterface.class); - } - - @Provides - @Singleton - public WikidataInterface provideWikidataInterface(CommonsServiceFactory serviceFactory) { - return serviceFactory.create(BuildConfig.WIKIDATA_URL, WikidataInterface.class); - } - - /** - * Add provider for PageMediaInterface - * It creates a retrofit service for the wiki site using device's current language - */ - @Provides - @Singleton - public PageMediaInterface providePageMediaInterface(@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) WikiSite wikiSite, CommonsServiceFactory serviceFactory) { - return serviceFactory.create(wikiSite.url(), PageMediaInterface.class); - } - - @Provides - @Singleton - @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) - public WikiSite provideLanguageWikipediaSite() { - return WikiSite.forLanguageCode(Locale.getDefault().getLanguage()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt new file mode 100644 index 0000000000..5ecc041209 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -0,0 +1,316 @@ +package fr.free.nrw.commons.di + +import android.content.Context +import com.google.gson.Gson +import dagger.Module +import dagger.Provides +import fr.free.nrw.commons.BetaConstants +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.OkHttpConnectionFactory +import fr.free.nrw.commons.actions.PageEditClient +import fr.free.nrw.commons.actions.PageEditInterface +import fr.free.nrw.commons.actions.ThanksInterface +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient +import fr.free.nrw.commons.auth.csrf.CsrfTokenInterface +import fr.free.nrw.commons.auth.csrf.LogoutClient +import fr.free.nrw.commons.auth.login.LoginClient +import fr.free.nrw.commons.auth.login.LoginInterface +import fr.free.nrw.commons.category.CategoryInterface +import fr.free.nrw.commons.explore.depictions.DepictsClient +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.media.MediaDetailInterface +import fr.free.nrw.commons.media.MediaInterface +import fr.free.nrw.commons.media.PageMediaInterface +import fr.free.nrw.commons.media.WikidataMediaInterface +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.mwapi.UserInterface +import fr.free.nrw.commons.notification.NotificationInterface +import fr.free.nrw.commons.review.ReviewInterface +import fr.free.nrw.commons.upload.UploadInterface +import fr.free.nrw.commons.upload.WikiBaseInterface +import fr.free.nrw.commons.upload.depicts.DepictsInterface +import fr.free.nrw.commons.wikidata.CommonsServiceFactory +import fr.free.nrw.commons.wikidata.GsonUtil +import fr.free.nrw.commons.wikidata.WikidataInterface +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieJar +import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage +import fr.free.nrw.commons.wikidata.model.WikiSite +import okhttp3.Cache +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.logging.HttpLoggingInterceptor.Level +import timber.log.Timber +import java.io.File +import java.util.Locale +import java.util.concurrent.TimeUnit +import javax.inject.Named +import javax.inject.Singleton + +@Module +@Suppress("unused") +class NetworkingModule { + @Provides + @Singleton + fun provideOkHttpClient( + context: Context, + httpLoggingInterceptor: HttpLoggingInterceptor + ): OkHttpClient = OkHttpClient.Builder() + .connectTimeout(120, TimeUnit.SECONDS) + .writeTimeout(120, TimeUnit.SECONDS) + .addInterceptor(httpLoggingInterceptor) + .readTimeout(120, TimeUnit.SECONDS) + .cache(Cache(File(context.cacheDir, "okHttpCache"), OK_HTTP_CACHE_SIZE)) + .build() + + @Provides + @Singleton + fun serviceFactory(cookieJar: CommonsCookieJar): CommonsServiceFactory = + CommonsServiceFactory(OkHttpConnectionFactory.getClient(cookieJar)) + + @Provides + @Singleton + fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor = + HttpLoggingInterceptor { message: String? -> + Timber.tag("OkHttp").v(message) + }.apply { + level = if (BuildConfig.DEBUG) Level.BODY else Level.BASIC + } + + @Provides + @Singleton + fun provideOkHttpJsonApiClient( + okHttpClient: OkHttpClient, + depictsClient: DepictsClient, + @Named("tools_forge") toolsForgeUrl: HttpUrl, + gson: Gson + ): OkHttpJsonApiClient = OkHttpJsonApiClient( + okHttpClient, depictsClient, toolsForgeUrl, WIKIDATA_SPARQL_QUERY_URL, + BuildConfig.WIKIMEDIA_CAMPAIGNS_URL, gson + ) + + @Provides + @Singleton + fun provideCookieStorage( + @Named("default_preferences") preferences: JsonKvStore + ): CommonsCookieStorage = CommonsCookieStorage(preferences).also { + it.load() + } + + @Provides + @Singleton + fun provideCookieJar(storage: CommonsCookieStorage): CommonsCookieJar = + CommonsCookieJar(storage) + + @Named(NAMED_COMMONS_CSRF) + @Provides + @Singleton + fun provideCommonsCsrfTokenClient( + sessionManager: SessionManager, + @Named("commons-csrf-interface") tokenInterface: CsrfTokenInterface, + loginClient: LoginClient, + logoutClient: LogoutClient + ): CsrfTokenClient = CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient) + + /** + * Provides a singleton instance of CsrfTokenClient for Wikidata. + * + * @param sessionManager The session manager to manage user sessions. + * @param tokenInterface The interface for obtaining CSRF tokens. + * @param loginClient The client for handling login operations. + * @param logoutClient The client for handling logout operations. + * @return A singleton instance of CsrfTokenClient. + */ + @Named(NAMED_WIKI_CSRF) + @Provides + @Singleton + fun provideWikiCsrfTokenClient( + sessionManager: SessionManager, + @Named("wikidata-csrf-interface") tokenInterface: CsrfTokenInterface, + loginClient: LoginClient, + logoutClient: LogoutClient + ): CsrfTokenClient = CsrfTokenClient(sessionManager, tokenInterface, loginClient, logoutClient) + + /** + * Provides a singleton instance of CsrfTokenInterface for Wikidata. + * + * @param factory The factory used to create service interfaces. + * @return A singleton instance of CsrfTokenInterface for Wikidata. + */ + @Named("wikidata-csrf-interface") + @Provides + @Singleton + fun provideWikidataCsrfTokenInterface(factory: CommonsServiceFactory): CsrfTokenInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + @Named("commons-csrf-interface") + @Provides + @Singleton + fun provideCsrfTokenInterface(factory: CommonsServiceFactory): CsrfTokenInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideLoginInterface(factory: CommonsServiceFactory): LoginInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideLoginClient(loginInterface: LoginInterface): LoginClient = + LoginClient(loginInterface) + + @Provides + @Named("tools_forge") + fun provideToolsForgeUrl(): HttpUrl = TOOLS_FORGE_URL.toHttpUrlOrNull()!! + + @Provides + @Singleton + @Named(NAMED_WIKI_DATA_WIKI_SITE) + fun provideWikidataWikiSite(): WikiSite = WikiSite(BuildConfig.WIKIDATA_URL) + + + /** + * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. + * @return returns a singleton Gson instance + */ + @Provides + @Singleton + fun provideGson(): Gson = GsonUtil.getDefaultGson() + + @Provides + @Singleton + fun provideReviewInterface(factory: CommonsServiceFactory): ReviewInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideDepictsInterface(factory: CommonsServiceFactory): DepictsInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + @Provides + @Singleton + fun provideWikiBaseInterface(factory: CommonsServiceFactory): WikiBaseInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideUploadInterface(factory: CommonsServiceFactory): UploadInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Named("commons-page-edit-service") + @Provides + @Singleton + fun providePageEditService(factory: CommonsServiceFactory): PageEditInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Named("wikidata-page-edit-service") + @Provides + @Singleton + fun provideWikiDataPageEditService(factory: CommonsServiceFactory): PageEditInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + @Named("commons-page-edit") + @Provides + @Singleton + fun provideCommonsPageEditClient( + @Named(NAMED_COMMONS_CSRF) csrfTokenClient: CsrfTokenClient, + @Named("commons-page-edit-service") pageEditInterface: PageEditInterface + ): PageEditClient = PageEditClient(csrfTokenClient, pageEditInterface) + + /** + * Provides a singleton instance of PageEditClient for Wikidata. + * + * @param csrfTokenClient The client used to manage CSRF tokens. + * @param pageEditInterface The interface for page edit operations. + * @return A singleton instance of PageEditClient for Wikidata. + */ + @Named("wikidata-page-edit") + @Provides + @Singleton + fun provideWikidataPageEditClient( + @Named(NAMED_WIKI_CSRF) csrfTokenClient: CsrfTokenClient, + @Named("wikidata-page-edit-service") pageEditInterface: PageEditInterface + ): PageEditClient = PageEditClient(csrfTokenClient, pageEditInterface) + + @Provides + @Singleton + fun provideMediaInterface(factory: CommonsServiceFactory): MediaInterface = + factory.create(BuildConfig.COMMONS_URL) + + /** + * Add provider for WikidataMediaInterface + * It creates a retrofit service for the commons wiki site + * @param commonsWikiSite commonsWikiSite + * @return WikidataMediaInterface + */ + @Provides + @Singleton + fun provideWikidataMediaInterface(factory: CommonsServiceFactory): WikidataMediaInterface = + factory.create(BetaConstants.COMMONS_URL) + + @Provides + @Singleton + fun providesMediaDetailInterface(factory: CommonsServiceFactory): MediaDetailInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideCategoryInterface(factory: CommonsServiceFactory): CategoryInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideThanksInterface(factory: CommonsServiceFactory): ThanksInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideNotificationInterface(factory: CommonsServiceFactory): NotificationInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideUserInterface(factory: CommonsServiceFactory): UserInterface = + factory.create(BuildConfig.COMMONS_URL) + + @Provides + @Singleton + fun provideWikidataInterface(factory: CommonsServiceFactory): WikidataInterface = + factory.create(BuildConfig.WIKIDATA_URL) + + /** + * Add provider for PageMediaInterface + * It creates a retrofit service for the wiki site using device's current language + */ + @Provides + @Singleton + fun providePageMediaInterface( + @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) wikiSite: WikiSite, + factory: CommonsServiceFactory + ): PageMediaInterface = factory.create(wikiSite.url()) + + @Provides + @Singleton + @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) + fun provideLanguageWikipediaSite(): WikiSite { + return WikiSite.forLanguageCode(Locale.getDefault().language) + } + + companion object { + private const val WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql" + private const val TOOLS_FORGE_URL = + "https://tools.wmflabs.org/commons-android-app/tool-commons-android-app" + + const val OK_HTTP_CACHE_SIZE: Long = (10 * 1024 * 1024).toLong() + + private const val NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite" + private const val NAMED_WIKI_PEDIA_WIKI_SITE = "wikipedia-wikisite" + + const val NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE: String = "language-wikipedia-wikisite" + + const val NAMED_COMMONS_CSRF: String = "commons-csrf" + const val NAMED_WIKI_CSRF: String = "wiki-csrf" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java deleted file mode 100644 index 1fb52c937a..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.java +++ /dev/null @@ -1,19 +0,0 @@ -package fr.free.nrw.commons.di; - -import dagger.Module; -import dagger.android.ContributesAndroidInjector; -import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService; - -/** - * This Class Represents the Module for dependency injection (using dagger) - * so, if a developer needs to add a new Service to the commons app - * then that must be mentioned here to inject the dependencies - */ -@Module -@SuppressWarnings({"WeakerAccess", "unused"}) -public abstract class ServiceBuilderModule { - - @ContributesAndroidInjector - abstract WikiAccountAuthenticatorService bindWikiAccountAuthenticatorService(); - -} diff --git a/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt new file mode 100644 index 0000000000..45dbd57211 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/ServiceBuilderModule.kt @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.di + +import dagger.Module +import dagger.android.ContributesAndroidInjector +import fr.free.nrw.commons.auth.WikiAccountAuthenticatorService + +/** + * This Class Represents the Module for dependency injection (using dagger) + * so, if a developer needs to add a new Service to the commons app + * then that must be mentioned here to inject the dependencies + */ +@Module +@Suppress("unused") +abstract class ServiceBuilderModule { + @ContributesAndroidInjector + abstract fun bindWikiAccountAuthenticatorService(): WikiAccountAuthenticatorService +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java index f810d0480f..f587893c5f 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java @@ -3,7 +3,10 @@ import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX; import com.google.gson.Gson; +import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.category.CategoryItem; +import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; +import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse; import io.reactivex.Single; import java.util.ArrayList; import java.util.Collections; @@ -11,14 +14,11 @@ import java.util.List; import java.util.Set; import javax.inject.Inject; -import javax.inject.Named; import okhttp3.HttpUrl; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse; import timber.log.Timber; /** @@ -30,14 +30,11 @@ public class CategoryApi { private final OkHttpClient okHttpClient; - private final String commonsBaseUrl; private final Gson gson; @Inject - public CategoryApi(OkHttpClient okHttpClient, Gson gson, - @Named("wikimedia_api_host") String commonsBaseUrl) { + public CategoryApi(final OkHttpClient okHttpClient, final Gson gson) { this.okHttpClient = okHttpClient; - this.commonsBaseUrl = commonsBaseUrl; this.gson = gson; } @@ -75,9 +72,9 @@ public Single> request(String coords) { * @param coords Coordinates to build query with * @return URL for API query */ - private HttpUrl buildUrl(String coords) { + private HttpUrl buildUrl(final String coords) { return HttpUrl - .parse(commonsBaseUrl) + .parse(BuildConfig.WIKIMEDIA_API_HOST) .newBuilder() .addQueryParameter("action", "query") .addQueryParameter("prop", "categories|coordinates|pageprops") diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java index 36c5585195..ecc9c19b54 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsPresenter.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.upload; +import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD; import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK; import android.content.Context; @@ -53,7 +54,7 @@ public class PendingUploadsPresenter implements UserActionListener { final ContributionsRemoteDataSource contributionsRemoteDataSource, final ContributionsRepository contributionsRepository, final UploadRepository uploadRepository, - @Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) { + @Named(IO_THREAD) final Scheduler ioThreadScheduler) { this.contributionBoundaryCallback = contributionBoundaryCallback; this.contributionsRepository = contributionsRepository; this.uploadRepository = uploadRepository; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 712f6fc3ed..210754bf4c 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt @@ -9,6 +9,8 @@ import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.category.CategoryEditHelper import fr.free.nrw.commons.category.CategoryItem import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.depicts.proxy import io.reactivex.Observable @@ -30,8 +32,8 @@ class CategoriesPresenter @Inject constructor( private val repository: UploadRepository, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, - @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioScheduler: Scheduler, + @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler, ) : CategoriesContract.UserActionListener { companion object { private val DUMMY: CategoriesContract.View = proxy() diff --git a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt index fa3eb354ee..4502e34347 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/depicts/DepictsPresenter.kt @@ -7,6 +7,8 @@ import fr.free.nrw.commons.Media import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.bookmarks.items.BookmarkItemsController import fr.free.nrw.commons.di.CommonsApplicationModule +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.IO_THREAD +import fr.free.nrw.commons.di.CommonsApplicationModule.Companion.MAIN_THREAD import fr.free.nrw.commons.repository.UploadRepository import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.wikidata.WikidataDisambiguationItems @@ -31,8 +33,8 @@ class DepictsPresenter @Inject constructor( private val repository: UploadRepository, - @param:Named(CommonsApplicationModule.IO_THREAD) private val ioScheduler: Scheduler, - @param:Named(CommonsApplicationModule.MAIN_THREAD) private val mainThreadScheduler: Scheduler, + @param:Named(IO_THREAD) private val ioScheduler: Scheduler, + @param:Named(MAIN_THREAD) private val mainThreadScheduler: Scheduler, ) : DepictsContract.UserActionListener { companion object { private val DUMMY = proxy() diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt index 39dbf0cadc..ca523a21fd 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt @@ -8,7 +8,7 @@ import retrofit2.converter.gson.GsonConverterFactory class CommonsServiceFactory( private val okHttpClient: OkHttpClient, ) { - private val builder: Retrofit.Builder by lazy { + val builder: Retrofit.Builder by lazy { // All instances of retrofit share this configuration, but create it lazily Retrofit .Builder() @@ -17,15 +17,11 @@ class CommonsServiceFactory( .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) } - private val retrofitCache: MutableMap = mutableMapOf() + val retrofitCache: MutableMap = mutableMapOf() - fun create( - baseUrl: String, - service: Class, - ): T = - retrofitCache - .getOrPut(baseUrl) { - // Cache instances of retrofit based on API backend - builder.baseUrl(baseUrl).build() - }.create(service) + inline fun create(baseUrl: String): T = + retrofitCache.getOrPut(baseUrl) { + // Cache instances of retrofit based on API backend + builder.baseUrl(baseUrl).build() + }.create(T::class.java) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index 4c38a30ff1..c0e3bda083 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -37,9 +37,8 @@ class TestCommonsApplication : Application() { } @Suppress("MemberVisibilityCanBePrivate") -class MockCommonsApplicationModule( - appContext: Context, -) : CommonsApplicationModule(appContext) { +class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModule(appContext) { + val defaultSharedPreferences: JsonKvStore = mock() val locationServiceManager: LocationServiceManager = mock() val mockDbOpenHelper: DBOpenHelper = mock() @@ -50,16 +49,13 @@ class MockCommonsApplicationModule( val modificationClient: ContentProviderClient = mock() val uploadPrefs: JsonKvStore = mock() - override fun provideCategoryContentProviderClient(context: Context?): ContentProviderClient = categoryClient + override fun provideCategoryContentProviderClient(context: Context): ContentProviderClient = categoryClient - override fun provideContributionContentProviderClient(context: Context?): ContentProviderClient = contributionClient + override fun provideContributionContentProviderClient(context: Context): ContentProviderClient = contributionClient - override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient + override fun provideModificationContentProviderClient(context: Context): ContentProviderClient = modificationClient - override fun providesDefaultKvStore( - context: Context, - gson: Gson, - ): JsonKvStore = defaultSharedPreferences + override fun providesDefaultKvStore(context: Context, gson: Gson): JsonKvStore = defaultSharedPreferences override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager From fb1ef3212daecdfa0334f29bcc1a18cd409a64cd Mon Sep 17 00:00:00 2001 From: Neel Doshi <60827173+neeldoshii@users.noreply.github.com> Date: Sat, 30 Nov 2024 07:21:53 +0530 Subject: [PATCH 044/231] Migrated Bookmark from `Java` to `Kotlin` (#5960) * Rename Bookmark Pages from `.java` to `.kt` * Migrated Bookmark Pages to kotlin --- .../nrw/commons/bookmarks/BookmarkPages.java | 32 ------------------- .../nrw/commons/bookmarks/BookmarkPages.kt | 8 +++++ 2 files changed, 8 insertions(+), 32 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java create mode 100644 app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java deleted file mode 100644 index 71690c5e29..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.bookmarks; - -import androidx.fragment.app.Fragment; - -/** - * Data class for handling a bookmark fragment and it title - */ -public class BookmarkPages { - private Fragment page; - private String title; - - BookmarkPages(Fragment fragment, String title) { - this.title = title; - this.page = fragment; - } - - /** - * Return the fragment - * @return fragment object - */ - public Fragment getPage() { - return page; - } - - /** - * Return the fragment title - * @return title - */ - public String getTitle() { - return title; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt new file mode 100644 index 0000000000..e0ade52fe7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/bookmarks/BookmarkPages.kt @@ -0,0 +1,8 @@ +package fr.free.nrw.commons.bookmarks + +import androidx.fragment.app.Fragment + +data class BookmarkPages ( + val page: Fragment? = null, + val title: String? = null +) \ No newline at end of file From 771f370f9a3e34d17d29c2b0d39e89d380d4e311 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Mon, 2 Dec 2024 13:24:26 +0530 Subject: [PATCH 045/231] Migration of locationpicker module from Java to Kotlin (#5981) * Rename .java to .kt * Migrated location picker module from Java to Kotlin --- .../LocationPicker/LocationPicker.java | 77 -- .../commons/LocationPicker/LocationPicker.kt | 72 ++ .../LocationPickerActivity.java | 681 ------------------ .../LocationPicker/LocationPickerActivity.kt | 678 +++++++++++++++++ .../LocationPickerConstants.java | 20 - .../LocationPicker/LocationPickerConstants.kt | 13 + .../LocationPickerViewModel.java | 63 -- .../LocationPicker/LocationPickerViewModel.kt | 44 ++ 8 files changed, 807 insertions(+), 841 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java create mode 100644 app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java deleted file mode 100644 index 58801c4992..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.java +++ /dev/null @@ -1,77 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -import android.app.Activity; -import android.content.Intent; -import fr.free.nrw.commons.CameraPosition; -import fr.free.nrw.commons.Media; - -/** - * Helper class for starting the activity - */ -public final class LocationPicker { - - /** - * Getting camera position from the intent using constants - * - * @param data intent - * @return CameraPosition - */ - public static CameraPosition getCameraPosition(final Intent data) { - return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); - } - - public static class IntentBuilder { - - private final Intent intent; - - /** - * Creates a new builder that creates an intent to launch the place picker activity. - */ - public IntentBuilder() { - intent = new Intent(); - } - - /** - * Gets and puts location in intent - * @param position CameraPosition - * @return LocationPicker.IntentBuilder - */ - public LocationPicker.IntentBuilder defaultLocation( - final CameraPosition position) { - intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position); - return this; - } - - /** - * Gets and puts activity name in intent - * @param activity activity key - * @return LocationPicker.IntentBuilder - */ - public LocationPicker.IntentBuilder activityKey( - final String activity) { - intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity); - return this; - } - - /** - * Gets and puts media in intent - * @param media Media - * @return LocationPicker.IntentBuilder - */ - public LocationPicker.IntentBuilder media( - final Media media) { - intent.putExtra(LocationPickerConstants.MEDIA, media); - return this; - } - - /** - * Gets and sets the activity - * @param activity Activity - * @return Intent - */ - public Intent build(final Activity activity) { - intent.setClass(activity, LocationPickerActivity.class); - return intent; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt new file mode 100644 index 0000000000..0bab502012 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPicker.kt @@ -0,0 +1,72 @@ +package fr.free.nrw.commons.LocationPicker + +import android.app.Activity +import android.content.Intent +import fr.free.nrw.commons.CameraPosition +import fr.free.nrw.commons.Media + + +/** + * Helper class for starting the activity + */ +object LocationPicker { + + /** + * Getting camera position from the intent using constants + * + * @param data intent + * @return CameraPosition + */ + @JvmStatic + fun getCameraPosition(data: Intent): CameraPosition? { + return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION) + } + + class IntentBuilder + /** + * Creates a new builder that creates an intent to launch the place picker activity. + */() { + + private val intent: Intent = Intent() + + /** + * Gets and puts location in intent + * @param position CameraPosition + * @return LocationPicker.IntentBuilder + */ + fun defaultLocation(position: CameraPosition): IntentBuilder { + intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position) + return this + } + + /** + * Gets and puts activity name in intent + * @param activity activity key + * @return LocationPicker.IntentBuilder + */ + fun activityKey(activity: String): IntentBuilder { + intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity) + return this + } + + /** + * Gets and puts media in intent + * @param media Media + * @return LocationPicker.IntentBuilder + */ + fun media(media: Media): IntentBuilder { + intent.putExtra(LocationPickerConstants.MEDIA, media) + return this + } + + /** + * Gets and sets the activity + * @param activity Activity + * @return Intent + */ + fun build(activity: Activity): Intent { + intent.setClass(activity, LocationPickerActivity::class.java) + return intent + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java deleted file mode 100644 index 40f360a243..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.java +++ /dev/null @@ -1,681 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION; -import static fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM; -import static fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL; - -import android.Manifest.permission; -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.drawable.Drawable; -import android.location.LocationManager; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.text.Html; -import android.text.method.LinkMovementMethod; -import android.view.MotionEvent; -import android.view.View; -import android.view.Window; -import android.view.animation.OvershootInterpolator; -import android.widget.Button; -import android.widget.ImageView; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.app.AppCompatActivity; -import androidx.appcompat.widget.AppCompatTextView; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import com.google.android.material.floatingactionbutton.FloatingActionButton; -import fr.free.nrw.commons.CameraPosition; -import fr.free.nrw.commons.CommonsApplication; -import fr.free.nrw.commons.Media; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.auth.csrf.CsrfTokenClient; -import fr.free.nrw.commons.coordinates.CoordinateEditHelper; -import fr.free.nrw.commons.filepicker.Constants; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.location.LocationPermissionsHelper; -import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback; -import fr.free.nrw.commons.location.LocationServiceManager; -import fr.free.nrw.commons.theme.BaseActivity; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.SystemThemeUtils; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.schedulers.Schedulers; -import java.util.List; -import java.util.Locale; -import javax.inject.Inject; -import javax.inject.Named; -import org.osmdroid.tileprovider.tilesource.TileSourceFactory; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.util.constants.GeoConstants; -import org.osmdroid.views.CustomZoomButtonsController; -import org.osmdroid.views.overlay.Marker; -import org.osmdroid.views.overlay.Overlay; -import org.osmdroid.views.overlay.ScaleDiskOverlay; -import org.osmdroid.views.overlay.TilesOverlay; -import timber.log.Timber; - -/** - * Helps to pick location and return the result with an intent - */ -public class LocationPickerActivity extends BaseActivity implements - LocationPermissionCallback { - /** - * coordinateEditHelper: helps to edit coordinates - */ - @Inject - CoordinateEditHelper coordinateEditHelper; - /** - * media : Media object - */ - private Media media; - /** - * cameraPosition : position of picker - */ - private CameraPosition cameraPosition; - /** - * markerImage : picker image - */ - private ImageView markerImage; - /** - * mapView : OSM Map - */ - private org.osmdroid.views.MapView mapView; - /** - * tvAttribution : credit - */ - private AppCompatTextView tvAttribution; - /** - * activity : activity key - */ - private String activity; - /** - * modifyLocationButton : button for start editing location - */ - Button modifyLocationButton; - /** - * removeLocationButton : button to remove location metadata - */ - Button removeLocationButton; - /** - * showInMapButton : button for showing in map - */ - TextView showInMapButton; - /** - * placeSelectedButton : fab for selecting location - */ - FloatingActionButton placeSelectedButton; - /** - * fabCenterOnLocation: button for center on location; - */ - FloatingActionButton fabCenterOnLocation; - /** - * shadow : imageview of shadow - */ - private ImageView shadow; - /** - * largeToolbarText : textView of shadow - */ - private TextView largeToolbarText; - /** - * smallToolbarText : textView of shadow - */ - private TextView smallToolbarText; - /** - * applicationKvStore : for storing values - */ - @Inject - @Named("default_preferences") - public - JsonKvStore applicationKvStore; - BasicKvStore store; - /** - * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly - */ - @Inject - SystemThemeUtils systemThemeUtils; - private boolean isDarkTheme; - private boolean moveToCurrentLocation; - - @Inject - LocationServiceManager locationManager; - LocationPermissionsHelper locationPermissionsHelper; - - @Inject - SessionManager sessionManager; - - /** - * Constants - */ - private static final String CAMERA_POS = "cameraPosition"; - private static final String ACTIVITY = "activity"; - - - @SuppressLint("ClickableViewAccessibility") - @Override - protected void onCreate(@Nullable final Bundle savedInstanceState) { - getWindow().requestFeature(Window.FEATURE_ACTION_BAR); - super.onCreate(savedInstanceState); - - isDarkTheme = systemThemeUtils.isDeviceInNightMode(); - moveToCurrentLocation = false; - store = new BasicKvStore(this, "LocationPermissions"); - - getWindow().requestFeature(Window.FEATURE_ACTION_BAR); - final ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.hide(); - } - setContentView(R.layout.activity_location_picker); - - if (savedInstanceState == null) { - cameraPosition = getIntent() - .getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION); - activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY); - media = getIntent().getParcelableExtra(LocationPickerConstants.MEDIA); - }else{ - cameraPosition = savedInstanceState.getParcelable(CAMERA_POS); - activity = savedInstanceState.getString(ACTIVITY); - media = savedInstanceState.getParcelable("sMedia"); - } - bindViews(); - addBackButtonListener(); - addPlaceSelectedButton(); - addCredits(); - getToolbarUI(); - addCenterOnGPSButton(); - - org.osmdroid.config.Configuration.getInstance().load(getApplicationContext(), - PreferenceManager.getDefaultSharedPreferences(getApplicationContext())); - - mapView.setTileSource(TileSourceFactory.WIKIMEDIA); - mapView.setTilesScaledToDpi(true); - mapView.setMultiTouchControls(true); - - org.osmdroid.config.Configuration.getInstance().getAdditionalHttpRequestProperties().put( - "Referer", "http://maps.wikimedia.org/" - ); - mapView.getZoomController().setVisibility(CustomZoomButtonsController.Visibility.NEVER); - mapView.getController().setZoom(ZOOM_LEVEL); - mapView.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_MOVE) { - if (markerImage.getTranslationY() == 0) { - markerImage.animate().translationY(-75) - .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); - } - } else if (event.getAction() == MotionEvent.ACTION_UP) { - markerImage.animate().translationY(0) - .setInterpolator(new OvershootInterpolator()).setDuration(250).start(); - } - return false; - }); - - if ("UploadActivity".equals(activity)) { - placeSelectedButton.setVisibility(View.GONE); - modifyLocationButton.setVisibility(View.VISIBLE); - removeLocationButton.setVisibility(View.VISIBLE); - showInMapButton.setVisibility(View.VISIBLE); - largeToolbarText.setText(getResources().getString(R.string.image_location)); - smallToolbarText.setText(getResources(). - getString(R.string.check_whether_location_is_correct)); - fabCenterOnLocation.setVisibility(View.GONE); - markerImage.setVisibility(View.GONE); - shadow.setVisibility(View.GONE); - assert cameraPosition != null; - showSelectedLocationMarker(new GeoPoint(cameraPosition.getLatitude(), - cameraPosition.getLongitude())); - } - setupMapView(); - } - - /** - * Moves the center of the map to the specified coordinates - * - */ - private void moveMapTo(double latitude, double longitude){ - if(mapView != null && mapView.getController() != null){ - GeoPoint point = new GeoPoint(latitude, longitude); - - mapView.getController().setCenter(point); - mapView.getController().animateTo(point); - } - } - - /** - * Moves the center of the map to the specified coordinates - * @param point The GeoPoint object which contains the coordinates to move to - */ - private void moveMapTo(GeoPoint point){ - if(point != null){ - moveMapTo(point.getLatitude(), point.getLongitude()); - } - } - - /** - * For showing credits - */ - private void addCredits() { - tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution))); - tvAttribution.setMovementMethod(LinkMovementMethod.getInstance()); - } - - /** - * For setting up Dark Theme - */ - private void darkThemeSetup() { - if (isDarkTheme) { - shadow.setColorFilter(Color.argb(255, 255, 255, 255)); - mapView.getOverlayManager().getTilesOverlay() - .setColorFilter(TilesOverlay.INVERT_COLORS); - } - } - - /** - * Clicking back button destroy locationPickerActivity - */ - private void addBackButtonListener() { - final ImageView backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button); - backButton.setOnClickListener(v -> { - finish(); - }); - - } - - /** - * Binds mapView and location picker icon - */ - private void bindViews() { - mapView = findViewById(R.id.map_view); - markerImage = findViewById(R.id.location_picker_image_view_marker); - tvAttribution = findViewById(R.id.tv_attribution); - modifyLocationButton = findViewById(R.id.modify_location); - removeLocationButton = findViewById(R.id.remove_location); - showInMapButton = findViewById(R.id.show_in_map); - showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase( - Locale.ROOT)); - shadow = findViewById(R.id.location_picker_image_view_shadow); - } - - /** - * Gets toolbar color - */ - private void getToolbarUI() { - final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar); - largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view); - smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view); - toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor)); - } - - private void setupMapView() { - requestLocationPermissions(); - - //If location metadata is available, move map to that location. - if(activity.equals("UploadActivity") || activity.equals("MediaActivity")){ - moveMapToMediaLocation(); - } else { - //If location metadata is not available, move map to device GPS location. - moveMapToGPSLocation(); - } - - modifyLocationButton.setOnClickListener(v -> onClickModifyLocation()); - removeLocationButton.setOnClickListener(v -> onClickRemoveLocation()); - showInMapButton.setOnClickListener(v -> showInMapApp()); - darkThemeSetup(); - } - - /** - * Handles onclick event of modifyLocationButton - */ - private void onClickModifyLocation() { - placeSelectedButton.setVisibility(View.VISIBLE); - modifyLocationButton.setVisibility(View.GONE); - removeLocationButton.setVisibility(View.GONE); - showInMapButton.setVisibility(View.GONE); - markerImage.setVisibility(View.VISIBLE); - shadow.setVisibility(View.VISIBLE); - largeToolbarText.setText(getResources().getString(R.string.choose_a_location)); - smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust)); - fabCenterOnLocation.setVisibility(View.VISIBLE); - removeSelectedLocationMarker(); - moveMapToMediaLocation(); - } - - /** - * Handles onclick event of removeLocationButton - */ - private void onClickRemoveLocation() { - DialogUtil.showAlertDialog(this, - getString(R.string.remove_location_warning_title), - getString(R.string.remove_location_warning_desc), - getString(R.string.continue_message), - getString(R.string.cancel), () -> removeLocationFromImage(), null); - } - - /** - * Method to remove the location from the picture - */ - private void removeLocationFromImage() { - if (media != null) { - getCompositeDisposable().add(coordinateEditHelper.makeCoordinatesEdit(getApplicationContext() - , media, "0.0", "0.0", "0.0f") - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Timber.d("Coordinates are removed from the image"); - })); - } - final Intent returningIntent = new Intent(); - setResult(AppCompatActivity.RESULT_OK, returningIntent); - finish(); - } - - /** - * Show the location in map app. Map will center on the location metadata, if available. - * If there is no location metadata, the map will center on the commons app map center. - */ - private void showInMapApp() { - fr.free.nrw.commons.location.LatLng position = null; - - if(activity.equals("UploadActivity") && cameraPosition != null){ - //location metadata is available - position = new fr.free.nrw.commons.location.LatLng(cameraPosition.getLatitude(), - cameraPosition.getLongitude(), 0.0f); - } else if(mapView != null){ - //location metadata is not available - position = new fr.free.nrw.commons.location.LatLng(mapView.getMapCenter().getLatitude(), - mapView.getMapCenter().getLongitude(), 0.0f); - } - - if(position != null){ - Utils.handleGeoCoordinates(this, position); - } - } - - /** - * Moves the center of the map to the media's location, if that data - * is available. - */ - private void moveMapToMediaLocation() { - if (cameraPosition != null) { - - GeoPoint point = new GeoPoint(cameraPosition.getLatitude(), - cameraPosition.getLongitude()); - - moveMapTo(point); - } - } - - /** - * Moves the center of the map to the device's GPS location, if that data is available. - */ - private void moveMapToGPSLocation(){ - if(locationManager != null){ - fr.free.nrw.commons.location.LatLng location = locationManager.getLastLocation(); - - if(location != null){ - GeoPoint point = new GeoPoint(location.getLatitude(), location.getLongitude()); - - moveMapTo(point); - } - } - } - - /** - * Select the preferable location - */ - private void addPlaceSelectedButton() { - placeSelectedButton = findViewById(R.id.location_chosen_button); - placeSelectedButton.setOnClickListener(view -> placeSelected()); - } - - /** - * Return the intent with required data - */ - void placeSelected() { - if (activity.equals("NoLocationUploadActivity")) { - applicationKvStore.putString(LAST_LOCATION, - mapView.getMapCenter().getLatitude() - + "," - + mapView.getMapCenter().getLongitude()); - applicationKvStore.putString(LAST_ZOOM, mapView.getZoomLevel() + ""); - } - - if (media == null) { - final Intent returningIntent = new Intent(); - returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, - new CameraPosition(mapView.getMapCenter().getLatitude(), - mapView.getMapCenter().getLongitude(), 14.0)); - setResult(AppCompatActivity.RESULT_OK, returningIntent); - } else { - updateCoordinates(String.valueOf(mapView.getMapCenter().getLatitude()), - String.valueOf(mapView.getMapCenter().getLongitude()), - String.valueOf(0.0f)); - } - - finish(); - } - - /** - * Fetched coordinates are replaced with existing coordinates by a POST API call. - * @param Latitude to be added - * @param Longitude to be added - * @param Accuracy to be added - */ - public void updateCoordinates(final String Latitude, final String Longitude, - final String Accuracy) { - if (media == null) { - return; - } - - try { - getCompositeDisposable().add( - coordinateEditHelper.makeCoordinatesEdit(getApplicationContext(), media, - Latitude, Longitude, Accuracy) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(s -> { - Timber.d("Coordinates are added."); - })); - } catch (Exception e) { - if (e.getLocalizedMessage().equals(CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE)) { - final String username = sessionManager.getUserName(); - final CommonsApplication.BaseLogoutListener logoutListener = new CommonsApplication.BaseLogoutListener( - this, - getString(R.string.invalid_login_message), - username - ); - - CommonsApplication.getInstance().clearApplicationData( - this, logoutListener); - } - } - } - - /** - * Center the camera on the last saved location - */ - private void addCenterOnGPSButton() { - fabCenterOnLocation = findViewById(R.id.center_on_gps); - fabCenterOnLocation.setOnClickListener(view -> { - moveToCurrentLocation = true; - requestLocationPermissions(); - }); - } - - /** - * Adds selected location marker on the map - */ - private void showSelectedLocationMarker(GeoPoint point) { - Drawable icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker); - Marker marker = new Marker(mapView); - marker.setPosition(point); - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM); - marker.setIcon(icon); - marker.setInfoWindow(null); - mapView.getOverlays().add(marker); - mapView.invalidate(); - } - - /** - * Removes selected location marker from the map - */ - private void removeSelectedLocationMarker() { - List overlays = mapView.getOverlays(); - for (int i = 0; i < overlays.size(); i++) { - if (overlays.get(i) instanceof Marker) { - Marker item = (Marker) overlays.get(i); - if (cameraPosition.getLatitude() == item.getPosition().getLatitude() - && cameraPosition.getLongitude() == item.getPosition().getLongitude()) { - mapView.getOverlays().remove(i); - mapView.invalidate(); - break; - } - } - } - } - - /** - * Center the map at user's current location - */ - private void requestLocationPermissions() { - locationPermissionsHelper = new LocationPermissionsHelper( - this, locationManager, this); - locationPermissionsHelper.requestForLocationAccess(R.string.location_permission_title, - R.string.upload_map_location_access); - } - - @Override - public void onRequestPermissionsResult(final int requestCode, - @NonNull final String[] permissions, - @NonNull final int[] grantResults) { - if (requestCode == Constants.RequestCodes.LOCATION - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - onLocationPermissionGranted(); - } else { - onLocationPermissionDenied(getString(R.string.upload_map_location_access)); - } - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - } - - @Override - protected void onResume() { - super.onResume(); - mapView.onResume(); - } - - @Override - protected void onPause() { - super.onPause(); - mapView.onPause(); - } - - @Override - public void onLocationPermissionDenied(String toastMessage) { - if (!ActivityCompat.shouldShowRequestPermissionRationale(this, - permission.ACCESS_FINE_LOCATION)) { - if (!locationPermissionsHelper.checkLocationPermission(this)) { - if (store.getBoolean("isPermissionDenied", false)) { - // means user has denied location permission twice or checked the "Don't show again" - locationPermissionsHelper.showAppSettingsDialog(this, - R.string.upload_map_location_access); - } else { - Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show(); - } - store.putBoolean("isPermissionDenied", true); - } - } else { - Toast.makeText(getBaseContext(), toastMessage, Toast.LENGTH_LONG).show(); - } - } - - @Override - public void onLocationPermissionGranted() { - if (moveToCurrentLocation || !(activity.equals("MediaActivity"))) { - if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { - locationManager.requestLocationUpdatesFromProvider( - LocationManager.NETWORK_PROVIDER); - locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - addMarkerAtGPSLocation(); - } else { - addMarkerAtGPSLocation(); - locationPermissionsHelper.showLocationOffDialog(this, - R.string.ask_to_turn_location_on_text); - } - } - } - - /** - * Adds a marker to the map at the most recent GPS location - * (which may be the current GPS location). - */ - private void addMarkerAtGPSLocation() { - fr.free.nrw.commons.location.LatLng currLocation = locationManager.getLastLocation(); - if (currLocation != null) { - GeoPoint currLocationGeopoint = new GeoPoint(currLocation.getLatitude(), - currLocation.getLongitude()); - addLocationMarker(currLocationGeopoint); - markerImage.setTranslationY(0); - } - } - - private void addLocationMarker(GeoPoint geoPoint) { - if (moveToCurrentLocation) { - mapView.getOverlays().clear(); - } - ScaleDiskOverlay diskOverlay = - new ScaleDiskOverlay(this, - geoPoint, 2000, GeoConstants.UnitOfMeasure.foot); - Paint circlePaint = new Paint(); - circlePaint.setColor(Color.rgb(128, 128, 128)); - circlePaint.setStyle(Paint.Style.STROKE); - circlePaint.setStrokeWidth(2f); - diskOverlay.setCirclePaint2(circlePaint); - Paint diskPaint = new Paint(); - diskPaint.setColor(Color.argb(40, 128, 128, 128)); - diskPaint.setStyle(Paint.Style.FILL_AND_STROKE); - diskOverlay.setCirclePaint1(diskPaint); - diskOverlay.setDisplaySizeMin(900); - diskOverlay.setDisplaySizeMax(1700); - mapView.getOverlays().add(diskOverlay); - org.osmdroid.views.overlay.Marker startMarker = new org.osmdroid.views.overlay.Marker( - mapView); - startMarker.setPosition(geoPoint); - startMarker.setAnchor(org.osmdroid.views.overlay.Marker.ANCHOR_CENTER, - org.osmdroid.views.overlay.Marker.ANCHOR_BOTTOM); - startMarker.setIcon( - ContextCompat.getDrawable(this, R.drawable.current_location_marker)); - startMarker.setTitle("Your Location"); - startMarker.setTextLabelFontSize(24); - mapView.getOverlays().add(startMarker); - } - - /** - * Saves the state of the activity - * @param outState Bundle - */ - @Override - public void onSaveInstanceState(@NonNull final Bundle outState) { - super.onSaveInstanceState(outState); - if(cameraPosition!=null){ - outState.putParcelable(CAMERA_POS, cameraPosition); - } - if(activity!=null){ - outState.putString(ACTIVITY, activity); - } - - if(media!=null){ - outState.putParcelable("sMedia", media); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt new file mode 100644 index 0000000000..6508c4f256 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt @@ -0,0 +1,678 @@ +package fr.free.nrw.commons.LocationPicker + +import android.Manifest.permission +import android.annotation.SuppressLint +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Color +import android.graphics.Paint +import android.location.LocationManager +import android.os.Bundle +import android.preference.PreferenceManager +import android.text.Html +import android.text.method.LinkMovementMethod +import android.view.MotionEvent +import android.view.View +import android.view.Window +import android.view.animation.OvershootInterpolator +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.appcompat.widget.AppCompatTextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import com.google.android.material.floatingactionbutton.FloatingActionButton +import fr.free.nrw.commons.CameraPosition +import fr.free.nrw.commons.CommonsApplication +import fr.free.nrw.commons.Media +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.auth.csrf.CsrfTokenClient +import fr.free.nrw.commons.coordinates.CoordinateEditHelper +import fr.free.nrw.commons.filepicker.Constants +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.location.LocationPermissionsHelper +import fr.free.nrw.commons.location.LocationPermissionsHelper.LocationPermissionCallback +import fr.free.nrw.commons.location.LocationServiceManager +import fr.free.nrw.commons.theme.BaseActivity +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_LOCATION +import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.LAST_ZOOM +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.MapUtils.ZOOM_LEVEL +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.util.constants.GeoConstants +import org.osmdroid.views.CustomZoomButtonsController +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.ScaleDiskOverlay +import org.osmdroid.views.overlay.TilesOverlay +import timber.log.Timber +import java.util.Locale +import javax.inject.Inject +import javax.inject.Named + + +/** + * Helps to pick location and return the result with an intent + */ +class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { + /** + * coordinateEditHelper: helps to edit coordinates + */ + @Inject + lateinit var coordinateEditHelper: CoordinateEditHelper + + /** + * media : Media object + */ + private var media: Media? = null + + /** + * cameraPosition : position of picker + */ + private var cameraPosition: CameraPosition? = null + + /** + * markerImage : picker image + */ + private lateinit var markerImage: ImageView + + /** + * mapView : OSM Map + */ + private var mapView: org.osmdroid.views.MapView? = null + + /** + * tvAttribution : credit + */ + private lateinit var tvAttribution: AppCompatTextView + + /** + * activity : activity key + */ + private var activity: String? = null + + /** + * modifyLocationButton : button for start editing location + */ + private lateinit var modifyLocationButton: Button + + /** + * removeLocationButton : button to remove location metadata + */ + private lateinit var removeLocationButton: Button + + /** + * showInMapButton : button for showing in map + */ + private lateinit var showInMapButton: TextView + + /** + * placeSelectedButton : fab for selecting location + */ + private lateinit var placeSelectedButton: FloatingActionButton + + /** + * fabCenterOnLocation: button for center on location; + */ + private lateinit var fabCenterOnLocation: FloatingActionButton + + /** + * shadow : imageview of shadow + */ + private lateinit var shadow: ImageView + + /** + * largeToolbarText : textView of shadow + */ + private lateinit var largeToolbarText: TextView + + /** + * smallToolbarText : textView of shadow + */ + private lateinit var smallToolbarText: TextView + + /** + * applicationKvStore : for storing values + */ + @Inject + @field: Named("default_preferences") + lateinit var applicationKvStore: JsonKvStore + private lateinit var store: BasicKvStore + + /** + * isDarkTheme: for keeping a track of the device theme and modifying the map theme accordingly + */ + private var isDarkTheme: Boolean = false + private var moveToCurrentLocation: Boolean = false + + @Inject + lateinit var locationManager: LocationServiceManager + private lateinit var locationPermissionsHelper: LocationPermissionsHelper + + @Inject + lateinit var sessionManager: SessionManager + + /** + * Constants + */ + companion object { + private const val CAMERA_POS = "cameraPosition" + private const val ACTIVITY = "activity" + } + + @SuppressLint("ClickableViewAccessibility") + override fun onCreate(savedInstanceState: Bundle?) { + requestWindowFeature(Window.FEATURE_ACTION_BAR) + super.onCreate(savedInstanceState) + + isDarkTheme = systemThemeUtils.isDeviceInNightMode() + moveToCurrentLocation = false + store = BasicKvStore(this, "LocationPermissions") + + requestWindowFeature(Window.FEATURE_ACTION_BAR) + supportActionBar?.hide() + setContentView(R.layout.activity_location_picker) + + if (savedInstanceState == null) { + cameraPosition = intent.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION) + activity = intent.getStringExtra(LocationPickerConstants.ACTIVITY_KEY) + media = intent.getParcelableExtra(LocationPickerConstants.MEDIA) + } else { + cameraPosition = savedInstanceState.getParcelable(CAMERA_POS) + activity = savedInstanceState.getString(ACTIVITY) + media = savedInstanceState.getParcelable("sMedia") + } + + bindViews() + addBackButtonListener() + addPlaceSelectedButton() + addCredits() + getToolbarUI() + addCenterOnGPSButton() + + org.osmdroid.config.Configuration.getInstance() + .load( + applicationContext, PreferenceManager.getDefaultSharedPreferences( + applicationContext + ) + ) + + mapView?.setTileSource(TileSourceFactory.WIKIMEDIA) + mapView?.setTilesScaledToDpi(true) + mapView?.setMultiTouchControls(true) + + org.osmdroid.config.Configuration.getInstance().additionalHttpRequestProperties["Referer"] = + "http://maps.wikimedia.org/" + mapView?.zoomController?.setVisibility(CustomZoomButtonsController.Visibility.NEVER) + mapView?.controller?.setZoom(ZOOM_LEVEL.toDouble()) + mapView?.setOnTouchListener { _, event -> + when (event.action) { + MotionEvent.ACTION_MOVE -> { + if (markerImage.translationY == 0f) { + markerImage.animate().translationY(-75f) + .setInterpolator(OvershootInterpolator()).duration = 250 + } + } + MotionEvent.ACTION_UP -> { + markerImage.animate().translationY(0f) + .setInterpolator(OvershootInterpolator()).duration = 250 + } + } + false + } + + if (activity == "UploadActivity") { + placeSelectedButton.visibility = View.GONE + modifyLocationButton.visibility = View.VISIBLE + removeLocationButton.visibility = View.VISIBLE + showInMapButton.visibility = View.VISIBLE + largeToolbarText.text = getString(R.string.image_location) + smallToolbarText.text = getString(R.string.check_whether_location_is_correct) + fabCenterOnLocation.visibility = View.GONE + markerImage.visibility = View.GONE + shadow.visibility = View.GONE + cameraPosition?.let { + showSelectedLocationMarker(GeoPoint(it.latitude, it.longitude)) + } + } + setupMapView() + } + + /** + * Moves the center of the map to the specified coordinates + */ + private fun moveMapTo(latitude: Double, longitude: Double) { + mapView?.controller?.let { + val point = GeoPoint(latitude, longitude) + it.setCenter(point) + it.animateTo(point) + } + } + + /** + * Moves the center of the map to the specified coordinates + * @param point The GeoPoint object which contains the coordinates to move to + */ + private fun moveMapTo(point: GeoPoint?) { + point?.let { + moveMapTo(it.latitude, it.longitude) + } + } + + /** + * For showing credits + */ + private fun addCredits() { + tvAttribution.text = Html.fromHtml(getString(R.string.map_attribution)) + tvAttribution.movementMethod = LinkMovementMethod.getInstance() + } + + /** + * For setting up Dark Theme + */ + private fun darkThemeSetup() { + if (isDarkTheme) { + shadow.setColorFilter(Color.argb(255, 255, 255, 255)) + mapView?.overlayManager?.tilesOverlay?.setColorFilter(TilesOverlay.INVERT_COLORS) + } + } + + /** + * Clicking back button destroy locationPickerActivity + */ + private fun addBackButtonListener() { + val backButton = findViewById(R.id.maplibre_place_picker_toolbar_back_button) + backButton.setOnClickListener { + finish() + } + } + + /** + * Binds mapView and location picker icon + */ + private fun bindViews() { + mapView = findViewById(R.id.map_view) + markerImage = findViewById(R.id.location_picker_image_view_marker) + tvAttribution = findViewById(R.id.tv_attribution) + modifyLocationButton = findViewById(R.id.modify_location) + removeLocationButton = findViewById(R.id.remove_location) + showInMapButton = findViewById(R.id.show_in_map) + showInMapButton.text = getString(R.string.show_in_map_app).uppercase(Locale.ROOT) + shadow = findViewById(R.id.location_picker_image_view_shadow) + } + + /** + * Gets toolbar color + */ + private fun getToolbarUI() { + val toolbar: ConstraintLayout = findViewById(R.id.location_picker_toolbar) + largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view) + smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view) + toolbar.setBackgroundColor(ContextCompat.getColor(this, R.color.primaryColor)) + } + + private fun setupMapView() { + requestLocationPermissions() + + //If location metadata is available, move map to that location. + if (activity == "UploadActivity" || activity == "MediaActivity") { + moveMapToMediaLocation() + } else { + //If location metadata is not available, move map to device GPS location. + moveMapToGPSLocation() + } + + modifyLocationButton.setOnClickListener { onClickModifyLocation() } + removeLocationButton.setOnClickListener { onClickRemoveLocation() } + showInMapButton.setOnClickListener { showInMapApp() } + darkThemeSetup() + } + + /** + * Handles onClick event of modifyLocationButton + */ + private fun onClickModifyLocation() { + placeSelectedButton.visibility = View.VISIBLE + modifyLocationButton.visibility = View.GONE + removeLocationButton.visibility = View.GONE + showInMapButton.visibility = View.GONE + markerImage.visibility = View.VISIBLE + shadow.visibility = View.VISIBLE + largeToolbarText.text = getString(R.string.choose_a_location) + smallToolbarText.text = getString(R.string.pan_and_zoom_to_adjust) + fabCenterOnLocation.visibility = View.VISIBLE + removeSelectedLocationMarker() + moveMapToMediaLocation() + } + + /** + * Handles onClick event of removeLocationButton + */ + private fun onClickRemoveLocation() { + DialogUtil.showAlertDialog( + this, + getString(R.string.remove_location_warning_title), + getString(R.string.remove_location_warning_desc), + getString(R.string.continue_message), + getString(R.string.cancel), + { removeLocationFromImage() }, + null + ) + } + + /** + * Removes location metadata from the image + */ + private fun removeLocationFromImage() { + media?.let { + compositeDisposable.add( + coordinateEditHelper.makeCoordinatesEdit( + applicationContext, it, "0.0", "0.0", "0.0f" + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { _ -> + Timber.d("Coordinates removed from the image") + } + ) + } + setResult(RESULT_OK, Intent()) + finish() + } + + /** + * Show location in map app + */ + private fun showInMapApp() { + val position = when { + //location metadata is available + activity == "UploadActivity" && cameraPosition != null -> { + fr.free.nrw.commons.location.LatLng(cameraPosition!!.latitude, cameraPosition!!.longitude, 0.0f) + } + //location metadata is not available + mapView != null -> { + fr.free.nrw.commons.location.LatLng( + mapView?.mapCenter?.latitude!!, + mapView?.mapCenter?.longitude!!, + 0.0f + ) + } + else -> null + } + + position?.let { Utils.handleGeoCoordinates(this, it) } + } + + /** + * Moves map to media's location + */ + private fun moveMapToMediaLocation() { + cameraPosition?.let { + moveMapTo(GeoPoint(it.latitude, it.longitude)) + } + } + + /** + * Moves map to GPS location + */ + private fun moveMapToGPSLocation() { + locationManager.lastLocation?.let { + moveMapTo(GeoPoint(it.latitude, it.longitude)) + } + } + + /** + * Adds "Place Selected" button + */ + private fun addPlaceSelectedButton() { + placeSelectedButton = findViewById(R.id.location_chosen_button) + placeSelectedButton.setOnClickListener { placeSelected() } + } + + /** + * Handles "Place Selected" action + */ + private fun placeSelected() { + if (activity == "NoLocationUploadActivity") { + applicationKvStore.putString( + LAST_LOCATION, + "${mapView?.mapCenter?.latitude},${mapView?.mapCenter?.longitude}" + ) + applicationKvStore.putString(LAST_ZOOM, mapView?.zoomLevel?.toString()!!) + } + + if (media == null) { + val intent = Intent().apply { + putExtra( + LocationPickerConstants.MAP_CAMERA_POSITION, + CameraPosition(mapView?.mapCenter?.latitude!!, mapView?.mapCenter?.longitude!!, 14.0) + ) + } + setResult(RESULT_OK, intent) + } else { + updateCoordinates( + mapView?.mapCenter?.latitude.toString(), + mapView?.mapCenter?.longitude.toString(), + "0.0f" + ) + } + + finish() + } + + /** + * Updates image with new coordinates + */ + fun updateCoordinates(latitude: String, longitude: String, accuracy: String) { + media?.let { + try { + compositeDisposable.add( + coordinateEditHelper.makeCoordinatesEdit( + applicationContext, + it, + latitude, + longitude, + accuracy + ).subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { _ -> + Timber.d("Coordinates updated") + } + ) + } catch (e: Exception) { + if (e.localizedMessage == CsrfTokenClient.ANONYMOUS_TOKEN_MESSAGE) { + val username = sessionManager.userName + CommonsApplication.BaseLogoutListener( + this, + getString(R.string.invalid_login_message) + , username + ).let { + CommonsApplication.instance.clearApplicationData(this, it) + } + } else { } + } + } + } + + /** + * Adds a button to center the map at user's location + */ + private fun addCenterOnGPSButton() { + fabCenterOnLocation = findViewById(R.id.center_on_gps) + fabCenterOnLocation.setOnClickListener { + moveToCurrentLocation = true + requestLocationPermissions() + } + } + + /** + * Shows a selected location marker + */ + private fun showSelectedLocationMarker(point: GeoPoint) { + val icon = ContextCompat.getDrawable(this, R.drawable.map_default_map_marker) + Marker(mapView).apply { + position = point + setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + setIcon(icon) + infoWindow = null + mapView?.overlays?.add(this) + } + mapView?.invalidate() + } + + /** + * Removes selected location marker + */ + private fun removeSelectedLocationMarker() { + val overlays = mapView?.overlays + overlays?.filterIsInstance()?.firstOrNull { + it.position.latitude == + cameraPosition?.latitude && it.position.longitude == cameraPosition?.longitude + }?.let { + overlays.remove(it) + mapView?.invalidate() + } + } + + /** + * Centers map at user's location + */ + private fun requestLocationPermissions() { + locationPermissionsHelper = LocationPermissionsHelper(this, locationManager, this) + locationPermissionsHelper.requestForLocationAccess( + R.string.location_permission_title, + R.string.upload_map_location_access + ) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + if (requestCode == Constants.RequestCodes.LOCATION && grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onLocationPermissionGranted() + } else { + onLocationPermissionDenied(getString(R.string.upload_map_location_access)) + } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + } + + override fun onResume() { + super.onResume() + mapView?.onResume() + } + + override fun onPause() { + super.onPause() + mapView?.onPause() + } + + override fun onLocationPermissionDenied(toastMessage: String) { + val isDeniedBefore = store.getBoolean("isPermissionDenied", false) + val showRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, permission.ACCESS_FINE_LOCATION) + + if (!showRationale) { + if (!locationPermissionsHelper.checkLocationPermission(this)) { + if (isDeniedBefore) { + locationPermissionsHelper.showAppSettingsDialog(this, R.string.upload_map_location_access) + } else { + Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show() + } + store.putBoolean("isPermissionDenied", true) + } + } else { + Toast.makeText(this, toastMessage, Toast.LENGTH_LONG).show() + } + } + + override fun onLocationPermissionGranted() { + if (moveToCurrentLocation || activity != "MediaActivity") { + if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn) { + locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) + locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + addMarkerAtGPSLocation() + } else { + addMarkerAtGPSLocation() + locationPermissionsHelper.showLocationOffDialog(this, R.string.ask_to_turn_location_on_text) + } + } + } + + /** + * Adds a marker at the user's GPS location + */ + private fun addMarkerAtGPSLocation() { + locationManager.lastLocation?.let { + addLocationMarker(GeoPoint(it.latitude, it.longitude)) + markerImage.translationY = 0f + } + } + + private fun addLocationMarker(geoPoint: GeoPoint) { + if (moveToCurrentLocation) { + mapView?.overlays?.clear() + } + + val diskOverlay = ScaleDiskOverlay( + this, + geoPoint, + 2000, + GeoConstants.UnitOfMeasure.foot + ) + + val circlePaint = Paint().apply { + color = Color.rgb(128, 128, 128) + style = Paint.Style.STROKE + strokeWidth = 2f + } + diskOverlay.setCirclePaint2(circlePaint) + + val diskPaint = Paint().apply { + color = Color.argb(40, 128, 128, 128) + style = Paint.Style.FILL_AND_STROKE + } + diskOverlay.setCirclePaint1(diskPaint) + + diskOverlay.setDisplaySizeMin(900) + diskOverlay.setDisplaySizeMax(1700) + + mapView?.overlays?.add(diskOverlay) + + val startMarker = Marker(mapView).apply { + position = geoPoint + setAnchor( + Marker.ANCHOR_CENTER, + Marker.ANCHOR_BOTTOM + ) + icon = ContextCompat.getDrawable(this@LocationPickerActivity, R.drawable.current_location_marker) + title = "Your Location" + textLabelFontSize = 24 + } + + mapView?.overlays?.add(startMarker) + } + + /** + * Saves the state of the activity + * @param outState Bundle + */ + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + cameraPosition?.let { + outState.putParcelable(CAMERA_POS, it) + } + + activity?.let { + outState.putString(ACTIVITY, it) + } + + media?.let { + outState.putParcelable("sMedia", it) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java deleted file mode 100644 index 060a15c88c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -/** - * Constants need for location picking - */ -public final class LocationPickerConstants { - - public static final String ACTIVITY_KEY - = "location.picker.activity"; - - public static final String MAP_CAMERA_POSITION - = "location.picker.cameraPosition"; - - public static final String MEDIA - = "location.picker.media"; - - - private LocationPickerConstants() { - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt new file mode 100644 index 0000000000..a1c9d989a4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerConstants.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.LocationPicker + +/** + * Constants need for location picking + */ +object LocationPickerConstants { + + const val ACTIVITY_KEY = "location.picker.activity" + + const val MAP_CAMERA_POSITION = "location.picker.cameraPosition" + + const val MEDIA = "location.picker.media" +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java deleted file mode 100644 index 57bb238d2b..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.LocationPicker; - -import android.app.Application; -import androidx.annotation.NonNull; -import androidx.lifecycle.AndroidViewModel; -import androidx.lifecycle.MutableLiveData; -import fr.free.nrw.commons.CameraPosition; -import org.jetbrains.annotations.NotNull; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; -import timber.log.Timber; - -/** - * Observes live camera position data - */ -public class LocationPickerViewModel extends AndroidViewModel implements Callback { - - /** - * Wrapping CameraPosition with MutableLiveData - */ - private final MutableLiveData result = new MutableLiveData<>(); - - /** - * Constructor for this class - * - * @param application Application - */ - public LocationPickerViewModel(@NonNull final Application application) { - super(application); - } - - /** - * Responses on camera position changing - * - * @param call Call - * @param response Response - */ - @Override - public void onResponse(final @NotNull Call call, - final Response response) { - if (response.body() == null) { - result.setValue(null); - return; - } - result.setValue(response.body()); - } - - @Override - public void onFailure(final @NotNull Call call, final @NotNull Throwable t) { - Timber.e(t); - } - - /** - * Gets live CameraPosition - * - * @return MutableLiveData - */ - public MutableLiveData getResult() { - return result; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt new file mode 100644 index 0000000000..b0b2ce6de4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerViewModel.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.LocationPicker + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.MutableLiveData +import fr.free.nrw.commons.CameraPosition +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import timber.log.Timber + +/** + * Observes live camera position data + */ +class LocationPickerViewModel( + application: Application +): AndroidViewModel(application), Callback { + + /** + * Wrapping CameraPosition with MutableLiveData + */ + val result = MutableLiveData() + + /** + * Responses on camera position changing + * + * @param call Call + * @param response Response + */ + override fun onResponse( + call: Call, + response: Response + ) { + if(response.body() == null) { + result.value = null + return + } + result.value = response.body() + } + + override fun onFailure(call: Call, t: Throwable) { + Timber.e(t) + } +} \ No newline at end of file From 8265cc6306c771ba3dd36abee37684c710700821 Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Tue, 3 Dec 2024 11:57:11 +0530 Subject: [PATCH 046/231] Migrate location and language module from Java to Kotlin (#5988) * Rename .java to .kt * Migrated location and language module from Java to Kotlin * Changed lastLocation visibility --- .../LocationPicker/LocationPickerActivity.kt | 6 +- .../language/AppLanguageLookUpTable.java | 141 --------- .../language/AppLanguageLookUpTable.kt | 135 +++++++++ .../fr/free/nrw/commons/location/LatLng.java | 198 ------------- .../fr/free/nrw/commons/location/LatLng.kt | 150 ++++++++++ .../location/LocationPermissionsHelper.java | 186 ------------ .../location/LocationPermissionsHelper.kt | 200 +++++++++++++ .../location/LocationServiceManager.java | 274 ------------------ .../location/LocationServiceManager.kt | 255 ++++++++++++++++ .../location/LocationUpdateListener.java | 7 - .../location/LocationUpdateListener.kt | 12 + .../nrw/commons/upload/LanguagesAdapter.kt | 16 +- .../kotlin/fr/free/nrw/commons/LatLngTests.kt | 2 +- .../media/MediaDetailFragmentUnitTests.kt | 8 +- .../commons/upload/LanguagesAdapterTest.kt | 10 +- 15 files changed, 773 insertions(+), 827 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java create mode 100644 app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LatLng.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LatLng.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java create mode 100644 app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt diff --git a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt index 6508c4f256..1a5ec0a348 100644 --- a/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/LocationPicker/LocationPickerActivity.kt @@ -423,7 +423,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { * Moves map to GPS location */ private fun moveMapToGPSLocation() { - locationManager.lastLocation?.let { + locationManager.getLastLocation()?.let { moveMapTo(GeoPoint(it.latitude, it.longitude)) } } @@ -591,7 +591,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { override fun onLocationPermissionGranted() { if (moveToCurrentLocation || activity != "MediaActivity") { - if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn) { + if (locationPermissionsHelper.isLocationAccessToAppsTurnedOn()) { locationManager.requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) locationManager.requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) addMarkerAtGPSLocation() @@ -606,7 +606,7 @@ class LocationPickerActivity : BaseActivity(), LocationPermissionCallback { * Adds a marker at the user's GPS location */ private fun addMarkerAtGPSLocation() { - locationManager.lastLocation?.let { + locationManager.getLastLocation()?.let { addLocationMarker(GeoPoint(it.latitude, it.longitude)) markerImage.translationY = 0f } diff --git a/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java b/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java deleted file mode 100644 index a0286a7ef5..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.java +++ /dev/null @@ -1,141 +0,0 @@ -package fr.free.nrw.commons.language; - -import android.content.Context; -import android.content.res.Resources; -import android.text.TextUtils; - -import androidx.annotation.ArrayRes; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import fr.free.nrw.commons.R; -import java.lang.ref.SoftReference; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -/** Immutable look up table for all app supported languages. All article languages may not be - * present in this table as it is statically bundled with the app. */ -public class AppLanguageLookUpTable { - public static final String SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans"; - public static final String TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant"; - public static final String CHINESE_CN_LANGUAGE_CODE = "zh-cn"; - public static final String CHINESE_HK_LANGUAGE_CODE = "zh-hk"; - public static final String CHINESE_MO_LANGUAGE_CODE = "zh-mo"; - public static final String CHINESE_SG_LANGUAGE_CODE = "zh-sg"; - public static final String CHINESE_TW_LANGUAGE_CODE = "zh-tw"; - public static final String CHINESE_YUE_LANGUAGE_CODE = "zh-yue"; - public static final String CHINESE_LANGUAGE_CODE = "zh"; - public static final String NORWEGIAN_LEGACY_LANGUAGE_CODE = "no"; - public static final String NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb"; - public static final String TEST_LANGUAGE_CODE = "test"; - public static final String FALLBACK_LANGUAGE_CODE = "en"; // Must exist in preference_language_keys. - - @NonNull private final Resources resources; - - // Language codes for all app supported languages in fixed order. The special code representing - // the dynamic system language is null. - @NonNull private SoftReference> codesRef = new SoftReference<>(null); - - // English names for all app supported languages in fixed order. - @NonNull private SoftReference> canonicalNamesRef = new SoftReference<>(null); - - // Native names for all app supported languages in fixed order. - @NonNull private SoftReference> localizedNamesRef = new SoftReference<>(null); - - public AppLanguageLookUpTable(@NonNull Context context) { - resources = context.getResources(); - } - - /** - * @return Nonnull immutable list. The special code representing the dynamic system language is - * null. - */ - @NonNull - public List getCodes() { - List codes = codesRef.get(); - if (codes == null) { - codes = getStringList(R.array.preference_language_keys); - codesRef = new SoftReference<>(codes); - } - return codes; - } - - @Nullable - public String getCanonicalName(@Nullable String code) { - String name = defaultIndex(getCanonicalNames(), indexOfCode(code), null); - if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) { - if (code.equals(Locale.CHINESE.getLanguage())) { - name = Locale.CHINESE.getDisplayName(Locale.ENGLISH); - } else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) { - name = defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null); - } - } - return name; - } - - @Nullable - public String getLocalizedName(@Nullable String code) { - String name = defaultIndex(getLocalizedNames(), indexOfCode(code), null); - if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(code)) { - if (code.equals(Locale.CHINESE.getLanguage())) { - name = Locale.CHINESE.getDisplayName(Locale.CHINESE); - } else if (code.equals(NORWEGIAN_LEGACY_LANGUAGE_CODE)) { - name = defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null); - } - } - return name; - } - - public List getCanonicalNames() { - List names = canonicalNamesRef.get(); - if (names == null) { - names = getStringList(R.array.preference_language_canonical_names); - canonicalNamesRef = new SoftReference<>(names); - } - return names; - } - - public List getLocalizedNames() { - List names = localizedNamesRef.get(); - if (names == null) { - names = getStringList(R.array.preference_language_local_names); - localizedNamesRef = new SoftReference<>(names); - } - return names; - } - - public boolean isSupportedCode(@Nullable String code) { - return getCodes().contains(code); - } - - private T defaultIndex(List list, int index, T defaultValue) { - return inBounds(list, index) ? list.get(index) : defaultValue; - } - - /** - * Searches #codes for the specified language code and returns the index for use in - * #canonicalNames and #localizedNames. - * - * @param code The language code to search for. The special code representing the dynamic system - * language is null. - * @return The index of the language code or -1 if the code is not supported. - */ - private int indexOfCode(@Nullable String code) { - return getCodes().indexOf(code); - } - - /** @return Nonnull immutable list. */ - @NonNull - private List getStringList(int id) { - return Arrays.asList(getStringArray(id)); - } - - private boolean inBounds(List list, int index) { - return index >= 0 && index < list.size(); - } - - public String[] getStringArray(@ArrayRes int id) { - return resources.getStringArray(id); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt b/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt new file mode 100644 index 0000000000..6809fd79cb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/language/AppLanguageLookUpTable.kt @@ -0,0 +1,135 @@ +package fr.free.nrw.commons.language + +import android.content.Context +import android.content.res.Resources +import android.text.TextUtils + +import androidx.annotation.ArrayRes +import fr.free.nrw.commons.R +import java.lang.ref.SoftReference +import java.util.Arrays +import java.util.Locale + + +/** Immutable look up table for all app supported languages. All article languages may not be + * present in this table as it is statically bundled with the app. */ +class AppLanguageLookUpTable(context: Context) { + + companion object { + const val SIMPLIFIED_CHINESE_LANGUAGE_CODE = "zh-hans" + const val TRADITIONAL_CHINESE_LANGUAGE_CODE = "zh-hant" + const val CHINESE_CN_LANGUAGE_CODE = "zh-cn" + const val CHINESE_HK_LANGUAGE_CODE = "zh-hk" + const val CHINESE_MO_LANGUAGE_CODE = "zh-mo" + const val CHINESE_SG_LANGUAGE_CODE = "zh-sg" + const val CHINESE_TW_LANGUAGE_CODE = "zh-tw" + const val CHINESE_YUE_LANGUAGE_CODE = "zh-yue" + const val CHINESE_LANGUAGE_CODE = "zh" + const val NORWEGIAN_LEGACY_LANGUAGE_CODE = "no" + const val NORWEGIAN_BOKMAL_LANGUAGE_CODE = "nb" + const val TEST_LANGUAGE_CODE = "test" + const val FALLBACK_LANGUAGE_CODE = "en" // Must exist in preference_language_keys. + } + + private val resources: Resources = context.resources + + // Language codes for all app supported languages in fixed order. The special code representing + // the dynamic system language is null. + private var codesRef = SoftReference>(null) + + // English names for all app supported languages in fixed order. + private var canonicalNamesRef = SoftReference>(null) + + // Native names for all app supported languages in fixed order. + private var localizedNamesRef = SoftReference>(null) + + /** + * @return Nonnull immutable list. The special code representing the dynamic system language is + * null. + */ + fun getCodes(): List { + var codes = codesRef.get() + if (codes == null) { + codes = getStringList(R.array.preference_language_keys) + codesRef = SoftReference(codes) + } + return codes + } + + fun getCanonicalName(code: String?): String? { + var name = defaultIndex(getCanonicalNames(), indexOfCode(code), null) + if (name.isNullOrEmpty() && !code.isNullOrEmpty()) { + name = when (code) { + Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.ENGLISH) + NORWEGIAN_LEGACY_LANGUAGE_CODE -> + defaultIndex(getCanonicalNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null) + else -> null + } + } + return name + } + + fun getLocalizedName(code: String?): String? { + var name = defaultIndex(getLocalizedNames(), indexOfCode(code), null) + if (name.isNullOrEmpty() && !code.isNullOrEmpty()) { + name = when (code) { + Locale.CHINESE.language -> Locale.CHINESE.getDisplayName(Locale.CHINESE) + NORWEGIAN_LEGACY_LANGUAGE_CODE -> + defaultIndex(getLocalizedNames(), indexOfCode(NORWEGIAN_BOKMAL_LANGUAGE_CODE), null) + else -> null + } + } + return name + } + + fun getCanonicalNames(): List { + var names = canonicalNamesRef.get() + if (names == null) { + names = getStringList(R.array.preference_language_canonical_names) + canonicalNamesRef = SoftReference(names) + } + return names + } + + fun getLocalizedNames(): List { + var names = localizedNamesRef.get() + if (names == null) { + names = getStringList(R.array.preference_language_local_names) + localizedNamesRef = SoftReference(names) + } + return names + } + + fun isSupportedCode(code: String?): Boolean { + return getCodes().contains(code) + } + + private fun defaultIndex(list: List, index: Int, defaultValue: T?): T? { + return if (inBounds(list, index)) list[index] else defaultValue + } + + /** + * Searches #codes for the specified language code and returns the index for use in + * #canonicalNames and #localizedNames. + * + * @param code The language code to search for. The special code representing the dynamic system + * language is null. + * @return The index of the language code or -1 if the code is not supported. + */ + private fun indexOfCode(code: String?): Int { + return getCodes().indexOf(code) + } + + /** @return Nonnull immutable list. */ + private fun getStringList(id: Int): List { + return getStringArray(id).toList() + } + + private fun inBounds(list: List<*>, index: Int): Boolean { + return index in list.indices + } + + fun getStringArray(@ArrayRes id: Int): Array { + return resources.getStringArray(id) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java b/app/src/main/java/fr/free/nrw/commons/location/LatLng.java deleted file mode 100644 index 4970fc54fc..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LatLng.java +++ /dev/null @@ -1,198 +0,0 @@ -package fr.free.nrw.commons.location; - -import android.location.Location; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; - -/** - * a latitude and longitude point with accuracy information, often of a picture - */ -public class LatLng implements Parcelable { - - private final double latitude; - private final double longitude; - private final float accuracy; - - /** - * Accepts latitude and longitude. - * North and South values are cut off at 90° - * - * @param latitude the latitude - * @param longitude the longitude - * @param accuracy the accuracy - * - * Examples: - * the Statue of Liberty is located at 40.69° N, 74.04° W - * The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0) - * where positive signifies north, east and negative signifies south, west. - */ - public LatLng(double latitude, double longitude, float accuracy) { - if (-180.0D <= longitude && longitude < 180.0D) { - this.longitude = longitude; - } else { - this.longitude = ((longitude - 180.0D) % 360.0D + 360.0D) % 360.0D - 180.0D; - } - this.latitude = Math.max(-90.0D, Math.min(90.0D, latitude)); - this.accuracy = accuracy; - } - /** - * An alternate constructor for this class. - * @param in A parcelable which contains the latitude, longitude, and accuracy - */ - public LatLng(Parcel in) { - latitude = in.readDouble(); - longitude = in.readDouble(); - accuracy = in.readFloat(); - } - - /** - * gets the latitude and longitude of a given non-null location - * @param location the non-null location of the user - * @return LatLng the Latitude and Longitude of a given location - */ - public static LatLng from(@NonNull Location location) { - return new LatLng(location.getLatitude(), location.getLongitude(), location.getAccuracy()); - } - - /** - * creates a hash code for the longitude and longitude - */ - public int hashCode() { - byte var1 = 1; - long var2 = Double.doubleToLongBits(this.latitude); - int var3 = 31 * var1 + (int)(var2 ^ var2 >>> 32); - var2 = Double.doubleToLongBits(this.longitude); - var3 = 31 * var3 + (int)(var2 ^ var2 >>> 32); - return var3; - } - - /** - * checks for equality of two LatLng objects - * @param o the second LatLng object - */ - public boolean equals(Object o) { - if (this == o) { - return true; - } else if (!(o instanceof LatLng)) { - return false; - } else { - LatLng var2 = (LatLng)o; - return Double.doubleToLongBits(this.latitude) == Double.doubleToLongBits(var2.latitude) && Double.doubleToLongBits(this.longitude) == Double.doubleToLongBits(var2.longitude); - } - } - - /** - * returns a string representation of the latitude and longitude - */ - public String toString() { - return "lat/lng: (" + this.latitude + "," + this.longitude + ")"; - } - - /** - * Rounds the float to 4 digits and returns absolute value. - * - * @param coordinate A coordinate value as string. - * @return String of the rounded number. - */ - private String formatCoordinate(double coordinate) { - double roundedNumber = Math.round(coordinate * 10000d) / 10000d; - double absoluteNumber = Math.abs(roundedNumber); - return String.valueOf(absoluteNumber); - } - - /** - * Returns "N" or "S" depending on the latitude. - * - * @return "N" or "S". - */ - private String getNorthSouth() { - if (this.latitude < 0) { - return "S"; - } - - return "N"; - } - - /** - * Returns "E" or "W" depending on the longitude. - * - * @return "E" or "W". - */ - private String getEastWest() { - if (this.longitude >= 0 && this.longitude < 180) { - return "E"; - } - - return "W"; - } - - /** - * Returns a nicely formatted coordinate string. Used e.g. in - * the detail view. - * - * @return The formatted string. - */ - public String getPrettyCoordinateString() { - return formatCoordinate(this.latitude) + " " + this.getNorthSouth() + ", " - + formatCoordinate(this.longitude) + " " + this.getEastWest(); - } - - /** - * Return the location accuracy in meter. - * - * @return float - */ - public float getAccuracy() { - return accuracy; - } - - /** - * Return the longitude in degrees. - * - * @return double - */ - public double getLongitude() { - return longitude; - } - - /** - * Return the latitude in degrees. - * - * @return double - */ - public double getLatitude() { - return latitude; - } - - public Uri getGmmIntentUri() { - return Uri.parse("geo:" + latitude + "," + longitude + "?z=16"); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeDouble(latitude); - dest.writeDouble(longitude); - dest.writeFloat(accuracy); - } - - public static final Creator CREATOR = new Creator() { - @Override - public LatLng createFromParcel(Parcel in) { - return new LatLng(in); - } - - @Override - public LatLng[] newArray(int size) { - return new LatLng[size]; - } - }; -} - diff --git a/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt b/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt new file mode 100644 index 0000000000..4e21b93c2e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LatLng.kt @@ -0,0 +1,150 @@ +package fr.free.nrw.commons.location + +import android.location.Location +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min +import kotlin.math.round + + +/** + * A latitude and longitude point with accuracy information, often of a picture. + */ +data class LatLng( + var latitude: Double, + var longitude: Double, + val accuracy: Float +) : Parcelable { + + /** + * Accepts latitude and longitude. + * North and South values are cut off at 90° + * + * Examples: + * the Statue of Liberty is located at 40.69° N, 74.04° W + * The Statue of Liberty could be constructed as LatLng(40.69, -74.04, 1.0) + * where positive signifies north, east and negative signifies south, west. + */ + init { + val adjustedLongitude = when { + longitude in -180.0..180.0 -> longitude + else -> ((longitude - 180.0) % 360.0 + 360.0) % 360.0 - 180.0 + } + latitude = max(-90.0, min(90.0, latitude)) + longitude = adjustedLongitude + } + + /** + * Accepts a non-null [Location] and converts it to a [LatLng]. + */ + companion object { + /** + * gets the latitude and longitude of a given non-null location + * @param location the non-null location of the user + * @return LatLng the Latitude and Longitude of a given location + */ + @JvmStatic + fun from(location: Location): LatLng { + return LatLng(location.latitude, location.longitude, location.accuracy) + } + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): LatLng { + return LatLng(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } + + /** + * An alternate constructor for this class. + * @param parcel A parcelable which contains the latitude, longitude, and accuracy + */ + private constructor(parcel: Parcel) : this( + latitude = parcel.readDouble(), + longitude = parcel.readDouble(), + accuracy = parcel.readFloat() + ) + + /** + * Creates a hash code for the latitude and longitude. + */ + override fun hashCode(): Int { + var result = 1 + val latitudeBits = latitude.toBits() + result = 31 * result + (latitudeBits xor (latitudeBits ushr 32)).toInt() + val longitudeBits = longitude.toBits() + result = 31 * result + (longitudeBits xor (longitudeBits ushr 32)).toInt() + return result + } + + /** + * Checks for equality of two LatLng objects. + * @param other the second LatLng object + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LatLng) return false + return latitude.toBits() == other.latitude.toBits() && + longitude.toBits() == other.longitude.toBits() + } + + /** + * Returns a string representation of the latitude and longitude. + */ + override fun toString(): String { + return "lat/lng: ($latitude,$longitude)" + } + + /** + * Returns a nicely formatted coordinate string. Used e.g. in + * the detail view. + * + * @return The formatted string. + */ + fun getPrettyCoordinateString(): String { + return "${formatCoordinate(latitude)} ${getNorthSouth()}, " + + "${formatCoordinate(longitude)} ${getEastWest()}" + } + + /** + * Gets a URI for a Google Maps intent at the location. + */ + fun getGmmIntentUri(): Uri { + return Uri.parse("geo:$latitude,$longitude?z=16") + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeDouble(latitude) + parcel.writeDouble(longitude) + parcel.writeFloat(accuracy) + } + + override fun describeContents(): Int = 0 + + private fun formatCoordinate(coordinate: Double): String { + val roundedNumber = round(coordinate * 10000) / 10000 + return abs(roundedNumber).toString() + } + + /** + * Returns "N" or "S" depending on the latitude. + * + * @return "N" or "S". + */ + private fun getNorthSouth(): String = if (latitude < 0) "S" else "N" + + /** + * Returns "E" or "W" depending on the longitude. + * + * @return "E" or "W". + */ + private fun getEastWest(): String = if (longitude in 0.0..179.999) "E" else "W" +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java deleted file mode 100644 index 77e089c9ca..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.java +++ /dev/null @@ -1,186 +0,0 @@ -package fr.free.nrw.commons.location; - -import android.Manifest; -import android.Manifest.permission; -import android.app.Activity; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.net.Uri; -import android.provider.Settings; -import android.widget.Toast; -import androidx.core.app.ActivityCompat; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.filepicker.Constants.RequestCodes; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.PermissionUtils; - -/** - * Helper class to handle location permissions. - * - * Location flow for fragments containing a map is as follows: - * Case 1: When location permission has never been asked for or denied before - * Check if permission is already granted or not. - * If not already granted, ask for it (if it isn't denied twice before). - * If now user grants permission, go to Case 3/4, else go to Case 2. - * - * Case 2: When location permission is just asked but has been denied - * Shows a toast to tell the user why location permission is needed. - * Also shows a rationale to the user, on agreeing to which, we go back to Case 1. - * Show current location / nearby pins / nearby images according to the default location. - * - * Case 3: When location permission are already granted, but location services are off - * Asks the user to turn on the location service, using a dialog. - * If the user rejects, checks for the last known location and shows stuff using that location. - * Also displays a toast telling the user why location should be turned on. - * - * Case 4: When location permission has been granted and location services are also on - * Do whatever is required by that particular activity / fragment using current location. - * - */ -public class LocationPermissionsHelper { - - Activity activity; - LocationServiceManager locationManager; - LocationPermissionCallback callback; - - public LocationPermissionsHelper(Activity activity, LocationServiceManager locationManager, - LocationPermissionCallback callback) { - this.activity = activity; - this.locationManager = locationManager; - this.callback = callback; - } - - /** - * Ask for location permission if the user agrees on attaching location with pictures and the - * app does not have the access to location - * - * @param dialogTitleResource Resource id of the title of the dialog - * @param dialogTextResource Resource id of the text of the dialog - */ - public void requestForLocationAccess( - int dialogTitleResource, - int dialogTextResource - ) { - if (checkLocationPermission(activity)) { - callback.onLocationPermissionGranted(); - } else { - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, - permission.ACCESS_FINE_LOCATION)) { - DialogUtil.showAlertDialog(activity, activity.getString(dialogTitleResource), - activity.getString(dialogTextResource), - activity.getString(android.R.string.ok), - activity.getString(android.R.string.cancel), - () -> { - ActivityCompat.requestPermissions(activity, - new String[]{permission.ACCESS_FINE_LOCATION}, 1); - }, - () -> callback.onLocationPermissionDenied( - activity.getString(R.string.upload_map_location_access)), - null, - false); - } else { - ActivityCompat.requestPermissions(activity, - new String[]{permission.ACCESS_FINE_LOCATION}, - RequestCodes.LOCATION); - } - } - } - - /** - * Shows a dialog for user to open the settings page and turn on location services - * - * @param activity Activity object - * @param dialogTextResource int id of the required string resource - */ - public void showLocationOffDialog(Activity activity, int dialogTextResource) { - DialogUtil - .showAlertDialog(activity, - activity.getString(R.string.ask_to_turn_location_on), - activity.getString(dialogTextResource), - activity.getString(R.string.title_app_shortcut_setting), - activity.getString(R.string.cancel), - () -> openLocationSettings(activity), - () -> Toast.makeText(activity, activity.getString(dialogTextResource), - Toast.LENGTH_LONG).show() - ); - } - - /** - * Opens the location access page in settings, for user to turn on location services - * - * @param activity Activtiy object - */ - public void openLocationSettings(Activity activity) { - final Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS); - final PackageManager packageManager = activity.getPackageManager(); - - if (intent.resolveActivity(packageManager) != null) { - activity.startActivity(intent); - } else { - Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG) - .show(); - } - } - - /** - * Shows a dialog for user to open the app's settings page and give location permission - * - * @param activity Activity object - * @param dialogTextResource int id of the required string resource - */ - public void showAppSettingsDialog(Activity activity, int dialogTextResource) { - DialogUtil - .showAlertDialog(activity, activity.getString(R.string.location_permission_title), - activity.getString(dialogTextResource), - activity.getString(R.string.title_app_shortcut_setting), - activity.getString(R.string.cancel), - () -> openAppSettings(activity), - () -> Toast.makeText(activity, activity.getString(dialogTextResource), - Toast.LENGTH_LONG).show() - ); - } - - /** - * Opens detailed settings page of the app for the user to turn on location services - * - * @param activity Activity object - */ - public void openAppSettings(Activity activity) { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - Uri uri = Uri.fromParts("package", activity.getPackageName(), null); - intent.setData(uri); - activity.startActivity(intent); - } - - - /** - * Check if apps have access to location even after having individual access - * - * @return Returns true if location services are on and false otherwise - */ - public boolean isLocationAccessToAppsTurnedOn() { - return (locationManager.isNetworkProviderEnabled() - || locationManager.isGPSProviderEnabled()); - } - - /** - * Checks if location permission is already granted or not - * - * @param activity Activity object - * @return Returns true if location permission is granted and false otherwise - */ - public boolean checkLocationPermission(Activity activity) { - return PermissionUtils.hasPermission(activity, - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}); - } - - /** - * Handle onPermissionDenied within individual classes based on the requirements - */ - public interface LocationPermissionCallback { - - void onLocationPermissionDenied(String toastMessage); - - void onLocationPermissionGranted(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt new file mode 100644 index 0000000000..771d9efdcf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationPermissionsHelper.kt @@ -0,0 +1,200 @@ +package fr.free.nrw.commons.location + +import android.Manifest.permission +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.Settings +import android.widget.Toast +import androidx.core.app.ActivityCompat +import fr.free.nrw.commons.R +import fr.free.nrw.commons.filepicker.Constants.RequestCodes +import fr.free.nrw.commons.utils.DialogUtil +import fr.free.nrw.commons.utils.PermissionUtils + +/** + * Helper class to handle location permissions. + * + * Location flow for fragments containing a map is as follows: + * Case 1: When location permission has never been asked for or denied before + * Check if permission is already granted or not. + * If not already granted, ask for it (if it isn't denied twice before). + * If now user grants permission, go to Case 3/4, else go to Case 2. + * + * Case 2: When location permission is just asked but has been denied + * Shows a toast to tell the user why location permission is needed. + * Also shows a rationale to the user, on agreeing to which, we go back to Case 1. + * Show current location / nearby pins / nearby images according to the default location. + * + * Case 3: When location permission are already granted, but location services are off + * Asks the user to turn on the location service, using a dialog. + * If the user rejects, checks for the last known location and shows stuff using that location. + * Also displays a toast telling the user why location should be turned on. + * + * Case 4: When location permission has been granted and location services are also on + * Do whatever is required by that particular activity / fragment using current location. + * + */ +class LocationPermissionsHelper( + private val activity: Activity, + private val locationManager: LocationServiceManager, + private val callback: LocationPermissionCallback? +) { + + /** + * Ask for location permission if the user agrees on attaching location with pictures and the + * app does not have the access to location + * + * @param dialogTitleResource Resource id of the title of the dialog + * @param dialogTextResource Resource id of the text of the dialog + */ + fun requestForLocationAccess( + dialogTitleResource: Int, + dialogTextResource: Int + ) { + if (checkLocationPermission(activity)) { + callback?.onLocationPermissionGranted() + } else { + if (ActivityCompat.shouldShowRequestPermissionRationale( + activity, + permission.ACCESS_FINE_LOCATION + ) + ) { + DialogUtil.showAlertDialog( + activity, + activity.getString(dialogTitleResource), + activity.getString(dialogTextResource), + activity.getString(android.R.string.ok), + activity.getString(android.R.string.cancel), + { + ActivityCompat.requestPermissions( + activity, + arrayOf(permission.ACCESS_FINE_LOCATION), + 1 + ) + }, + { + callback?.onLocationPermissionDenied( + activity.getString(R.string.upload_map_location_access) + ) + }, + null, + false + ) + } else { + ActivityCompat.requestPermissions( + activity, + arrayOf(permission.ACCESS_FINE_LOCATION), + RequestCodes.LOCATION + ) + } + } + } + + /** + * Shows a dialog for user to open the settings page and turn on location services + * + * @param activity Activity object + * @param dialogTextResource int id of the required string resource + */ + fun showLocationOffDialog(activity: Activity, dialogTextResource: Int) { + DialogUtil.showAlertDialog( + activity, + activity.getString(R.string.ask_to_turn_location_on), + activity.getString(dialogTextResource), + activity.getString(R.string.title_app_shortcut_setting), + activity.getString(R.string.cancel), + { openLocationSettings(activity) }, + { + Toast.makeText( + activity, + activity.getString(dialogTextResource), + Toast.LENGTH_LONG + ).show() + } + ) + } + + /** + * Opens the location access page in settings, for user to turn on location services + * + * @param activity Activity object + */ + fun openLocationSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) + val packageManager = activity.packageManager + + if (intent.resolveActivity(packageManager) != null) { + activity.startActivity(intent) + } else { + Toast.makeText(activity, R.string.cannot_open_location_settings, Toast.LENGTH_LONG) + .show() + } + } + + /** + * Shows a dialog for user to open the app's settings page and give location permission + * + * @param activity Activity object + * @param dialogTextResource int id of the required string resource + */ + fun showAppSettingsDialog(activity: Activity, dialogTextResource: Int) { + DialogUtil.showAlertDialog( + activity, + activity.getString(R.string.location_permission_title), + activity.getString(dialogTextResource), + activity.getString(R.string.title_app_shortcut_setting), + activity.getString(R.string.cancel), + { openAppSettings(activity) }, + { + Toast.makeText( + activity, + activity.getString(dialogTextResource), + Toast.LENGTH_LONG + ).show() + } + ) + } + + /** + * Opens detailed settings page of the app for the user to turn on location services + * + * @param activity Activity object + */ + private fun openAppSettings(activity: Activity) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + val uri = Uri.fromParts("package", activity.packageName, null) + intent.data = uri + activity.startActivity(intent) + } + + /** + * Check if apps have access to location even after having individual access + * + * @return Returns true if location services are on and false otherwise + */ + fun isLocationAccessToAppsTurnedOn(): Boolean { + return locationManager.isNetworkProviderEnabled() || locationManager.isGPSProviderEnabled() + } + + /** + * Checks if location permission is already granted or not + * + * @param activity Activity object + * @return Returns true if location permission is granted and false otherwise + */ + fun checkLocationPermission(activity: Activity): Boolean { + return PermissionUtils.hasPermission( + activity, + arrayOf(permission.ACCESS_FINE_LOCATION) + ) + } + + /** + * Handle onPermissionDenied within individual classes based on the requirements + */ + interface LocationPermissionCallback { + fun onLocationPermissionDenied(toastMessage: String) + fun onLocationPermissionGranted() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java deleted file mode 100644 index 4c7289ea50..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ /dev/null @@ -1,274 +0,0 @@ -package fr.free.nrw.commons.location; - -import android.Manifest.permission; -import android.app.Activity; -import android.content.Context; -import android.content.pm.PackageManager; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; -import android.os.Bundle; -import androidx.core.app.ActivityCompat; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; - -import timber.log.Timber; - -public class LocationServiceManager implements LocationListener { - - // Maybe these values can be improved for efficiency - private static final long MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100; - private static final long MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1; - - private LocationManager locationManager; - private Location lastLocation; - //private Location lastLocationDuplicate; // Will be used for nearby card view on contributions activity - private final List locationListeners = new CopyOnWriteArrayList<>(); - private boolean isLocationManagerRegistered = false; - private Set locationExplanationDisplayed = new HashSet<>(); - private Context context; - - /** - * Constructs a new instance of LocationServiceManager. - * - * @param context the context - */ - public LocationServiceManager(Context context) { - this.context = context; - this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - } - - public LatLng getLastLocation() { - if (lastLocation == null) { - lastLocation = getLastKnownLocation(); - if(lastLocation != null) { - return LatLng.from(lastLocation); - } - else { - return null; - } - } - return LatLng.from(lastLocation); - } - - private Location getLastKnownLocation() { - List providers = locationManager.getProviders(true); - Location bestLocation = null; - for (String provider : providers) { - Location l=null; - if (ActivityCompat.checkSelfPermission(context, permission.ACCESS_FINE_LOCATION) - == PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(context, permission.ACCESS_COARSE_LOCATION) - == PackageManager.PERMISSION_GRANTED) { - l = locationManager.getLastKnownLocation(provider); - } - if (l == null) { - continue; - } - if (bestLocation == null - || l.getAccuracy() < bestLocation.getAccuracy()) { - bestLocation = l; - } - } - if (bestLocation == null) { - return null; - } - return bestLocation; - } - - /** - * Registers a LocationManager to listen for current location. - */ - public void registerLocationManager() { - if (!isLocationManagerRegistered) { - isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) - && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); - } - } - - /** - * Requests location updates from the specified provider. - * - * @param locationProvider the location provider - * @return true if successful - */ - public boolean requestLocationUpdatesFromProvider(String locationProvider) { - try { - // If both providers are not available - if (locationManager == null || !(locationManager.getAllProviders().contains(locationProvider))) { - return false; - } - locationManager.requestLocationUpdates(locationProvider, - MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS, - MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS, - this); - return true; - } catch (IllegalArgumentException e) { - Timber.e(e, "Illegal argument exception"); - return false; - } catch (SecurityException e) { - Timber.e(e, "Security exception"); - return false; - } - } - - /** - * Returns whether a given location is better than the current best location. - * - * @param location the location to be tested - * @param currentBestLocation the current best location - * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly - * LOCATION_SLIGHTLY_CHANGED if location changed slightly - */ - private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { - - if (currentBestLocation == null) { - // A new location is always better than no location - return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; - } - - // Check whether the new location fix is newer or older - long timeDelta = location.getTime() - currentBestLocation.getTime(); - boolean isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS; - boolean isNewer = timeDelta > 0; - - // Check whether the new location fix is more or less accurate - int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy()); - boolean isLessAccurate = accuracyDelta > 0; - boolean isMoreAccurate = accuracyDelta < 0; - boolean isSignificantlyLessAccurate = accuracyDelta > 200; - - // Check if the old and new location are from the same provider - boolean isFromSameProvider = isSameProvider(location.getProvider(), - currentBestLocation.getProvider()); - - float[] results = new float[5]; - Location.distanceBetween( - currentBestLocation.getLatitude(), - currentBestLocation.getLongitude(), - location.getLatitude(), - location.getLongitude(), - results); - - // If it's been more than two minutes since the current location, use the new location - // because the user has likely moved - if (isSignificantlyNewer - || isMoreAccurate - || (isNewer && !isLessAccurate) - || (isNewer && !isSignificantlyLessAccurate && isFromSameProvider)) { - if (results[0] < 1000) { // Means change is smaller than 1000 meter - return LocationChangeType.LOCATION_SLIGHTLY_CHANGED; - } else { - return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED; - } - } else{ - return LocationChangeType.LOCATION_NOT_CHANGED; - } - } - - /** - * Checks whether two providers are the same - */ - private boolean isSameProvider(String provider1, String provider2) { - if (provider1 == null) { - return provider2 == null; - } - return provider1.equals(provider2); - } - - /** - * Unregisters location manager. - */ - public void unregisterLocationManager() { - isLocationManagerRegistered = false; - locationExplanationDisplayed.clear(); - try { - locationManager.removeUpdates(this); - } catch (SecurityException e) { - Timber.e(e, "Security exception"); - } - } - - /** - * Adds a new listener to the list of location listeners. - * - * @param listener the new listener - */ - public void addLocationListener(LocationUpdateListener listener) { - if (!locationListeners.contains(listener)) { - locationListeners.add(listener); - } - } - - /** - * Removes a listener from the list of location listeners. - * - * @param listener the listener to be removed - */ - public void removeLocationListener(LocationUpdateListener listener) { - locationListeners.remove(listener); - } - - @Override - public void onLocationChanged(Location location) { - Timber.d("on location changed"); - if (isBetterLocation(location, lastLocation) - .equals(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)) { - lastLocation = location; - //lastLocationDuplicate = location; - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChangedSignificantly(LatLng.from(lastLocation)); - } - } else if (location.distanceTo(lastLocation) >= 500) { - // Update nearby notification card at every 500 meters. - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChangedMedium(LatLng.from(lastLocation)); - } - } - - else if (isBetterLocation(location, lastLocation) - .equals(LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { - lastLocation = location; - //lastLocationDuplicate = location; - for (LocationUpdateListener listener : locationListeners) { - listener.onLocationChangedSlightly(LatLng.from(lastLocation)); - } - } - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - Timber.d("%s's status changed to %d", provider, status); - } - - @Override - public void onProviderEnabled(String provider) { - Timber.d("Provider %s enabled", provider); - } - - @Override - public void onProviderDisabled(String provider) { - Timber.d("Provider %s disabled", provider); - } - - public boolean isNetworkProviderEnabled() { - return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER); - } - - public boolean isGPSProviderEnabled() { - return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); - } - - public enum LocationChangeType{ - LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers - LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving - LOCATION_MEDIUM_CHANGED, //Between slight and significant changes, will be used for nearby card view updates. - LOCATION_NOT_CHANGED, - PERMISSION_JUST_GRANTED, - MAP_UPDATED, - SEARCH_CUSTOM_AREA, - CUSTOM_QUERY - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt new file mode 100644 index 0000000000..3a4c4b72e3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.kt @@ -0,0 +1,255 @@ +package fr.free.nrw.commons.location + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.location.LocationListener +import android.location.LocationManager +import android.os.Bundle +import androidx.core.app.ActivityCompat +import timber.log.Timber +import java.util.concurrent.CopyOnWriteArrayList + + +class LocationServiceManager(private val context: Context) : LocationListener { + + companion object { + // Maybe these values can be improved for efficiency + private const val MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS = 10 * 100L + private const val MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS = 1f + } + + private val locationManager: LocationManager = + context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + private var lastLocationVar: Location? = null + private val locationListeners = CopyOnWriteArrayList() + private var isLocationManagerRegistered = false + private val locationExplanationDisplayed = mutableSetOf() + + /** + * Constructs a new instance of LocationServiceManager. + * + */ + fun getLastLocation(): LatLng? { + if (lastLocationVar == null) { + lastLocationVar = getLastKnownLocation() + return lastLocationVar?.let { LatLng.from(it) } + } + return LatLng.from(lastLocationVar!!) + } + + private fun getLastKnownLocation(): Location? { + val providers = locationManager.getProviders(true) + var bestLocation: Location? = null + for (provider in providers) { + val location: Location? = if ( + ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION) + == + PackageManager.PERMISSION_GRANTED && + ActivityCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION) + == + PackageManager.PERMISSION_GRANTED + ) { + locationManager.getLastKnownLocation(provider) + } else { + null + } + + if ( + location != null + && + (bestLocation == null || location.accuracy < bestLocation.accuracy) + ) { + bestLocation = location + } + } + return bestLocation + } + + /** + * Registers a LocationManager to listen for current location. + */ + fun registerLocationManager() { + if (!isLocationManagerRegistered) { + isLocationManagerRegistered = + requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) && + requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER) + } + } + + /** + * Requests location updates from the specified provider. + * + * @param locationProvider the location provider + * @return true if successful + */ + fun requestLocationUpdatesFromProvider(locationProvider: String): Boolean { + return try { + if (locationManager.allProviders.contains(locationProvider)) { + locationManager.requestLocationUpdates( + locationProvider, + MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS, + MIN_LOCATION_UPDATE_REQUEST_DISTANCE_IN_METERS, + this + ) + true + } else { + false + } + } catch (e: IllegalArgumentException) { + Timber.e(e, "Illegal argument exception") + false + } catch (e: SecurityException) { + Timber.e(e, "Security exception") + false + } + } + + /** + * Returns whether a given location is better than the current best location. + * + * @param location the location to be tested + * @param currentBestLocation the current best location + * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly + * LOCATION_SLIGHTLY_CHANGED if location changed slightly + */ + private fun isBetterLocation(location: Location, currentBestLocation: Location?): LocationChangeType { + if (currentBestLocation == null) { + return LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + } + + val timeDelta = location.time - currentBestLocation.time + val isSignificantlyNewer = timeDelta > MIN_LOCATION_UPDATE_REQUEST_TIME_IN_MILLIS + val isNewer = timeDelta > 0 + val accuracyDelta = (location.accuracy - currentBestLocation.accuracy).toInt() + val isMoreAccurate = accuracyDelta < 0 + val isSignificantlyLessAccurate = accuracyDelta > 200 + val isFromSameProvider = isSameProvider(location.provider, currentBestLocation.provider) + + val results = FloatArray(5) + Location.distanceBetween( + currentBestLocation.latitude, currentBestLocation.longitude, + location.latitude, location.longitude, + results + ) + + return when { + isSignificantlyNewer + || + isMoreAccurate + || + (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) -> { + if (results[0] < 1000) LocationChangeType.LOCATION_SLIGHTLY_CHANGED + else LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED + } + else -> LocationChangeType.LOCATION_NOT_CHANGED + } + } + + /** + * Checks whether two providers are the same + */ + private fun isSameProvider(provider1: String?, provider2: String?): Boolean { + return provider1 == provider2 + } + + /** + * Unregisters location manager. + */ + fun unregisterLocationManager() { + isLocationManagerRegistered = false + locationExplanationDisplayed.clear() + try { + locationManager.removeUpdates(this) + } catch (e: SecurityException) { + Timber.e(e, "Security exception") + } + } + + /** + * Adds a new listener to the list of location listeners. + * + * @param listener the new listener + */ + fun addLocationListener(listener: LocationUpdateListener) { + if (!locationListeners.contains(listener)) { + locationListeners.add(listener) + } + } + + /** + * Removes a listener from the list of location listeners. + * + * @param listener the listener to be removed + */ + fun removeLocationListener(listener: LocationUpdateListener) { + locationListeners.remove(listener) + } + + override fun onLocationChanged(location: Location) { + Timber.d("on location changed") + val changeType = isBetterLocation(location, lastLocationVar) + if (changeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) { + lastLocationVar = location + locationListeners.forEach { it.onLocationChangedSignificantly(LatLng.from(location)) } + } else if (lastLocationVar?.let { location.distanceTo(it) }!! >= 500) { + locationListeners.forEach { it.onLocationChangedMedium(LatLng.from(location)) } + } else if (changeType == LocationChangeType.LOCATION_SLIGHTLY_CHANGED) { + lastLocationVar = location + locationListeners.forEach { it.onLocationChangedSlightly(LatLng.from(location)) } + } + } + + @Deprecated("Deprecated in Java", ReplaceWith( + "Timber.d(\"%s's status changed to %d\", provider, status)", + "timber.log.Timber" + ) + ) + override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) { + Timber.d("%s's status changed to %d", provider, status) + } + + + + override fun onProviderEnabled(provider: String) { + Timber.d("Provider %s enabled", provider) + } + + override fun onProviderDisabled(provider: String) { + Timber.d("Provider %s disabled", provider) + } + + fun isNetworkProviderEnabled(): Boolean { + return locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } + + fun isGPSProviderEnabled(): Boolean { + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) + } + + enum class LocationChangeType { + LOCATION_SIGNIFICANTLY_CHANGED, + LOCATION_SLIGHTLY_CHANGED, + LOCATION_MEDIUM_CHANGED, + LOCATION_NOT_CHANGED, + PERMISSION_JUST_GRANTED, + MAP_UPDATED, + SEARCH_CUSTOM_AREA, + CUSTOM_QUERY + } +} + + + + + + + + + diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java deleted file mode 100644 index 61ff26b11c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.location; - -public interface LocationUpdateListener { - void onLocationChangedSignificantly(LatLng latLng); // Will be used to update all nearby markers on the map - void onLocationChangedSlightly(LatLng latLng); // Will be used to track users motion - void onLocationChangedMedium(LatLng latLng); // Will be used updating nearby card view notification -} diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt new file mode 100644 index 0000000000..e90cc12241 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationUpdateListener.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.location + +interface LocationUpdateListener { + // Will be used to update all nearby markers on the map + fun onLocationChangedSignificantly(latLng: LatLng) + + // Will be used to track users motion + fun onLocationChangedSlightly(latLng: LatLng) + + // Will be used updating nearby card view notification + fun onLocationChangedMedium(latLng: LatLng) +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt index 2847fa0c0b..fa825d0a67 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/LanguagesAdapter.kt @@ -42,8 +42,8 @@ class LanguagesAdapter constructor( AppLanguageLookUpTable(context) init { - languageNamesList = language.localizedNames - languageCodesList = language.codes + languageNamesList = language.getLocalizedNames() + languageCodesList = language.getCodes() } private val filter = LanguageFilter() @@ -117,7 +117,7 @@ class LanguagesAdapter constructor( */ fun getIndexOfUserDefaultLocale(context: Context): Int { val userLanguageCode = context.locale?.language ?: return DEFAULT_INDEX - return language.codes.indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX + return language.getCodes().indexOf(userLanguageCode).takeIf { it >= 0 } ?: DEFAULT_INDEX } fun getIndexOfLanguageCode(languageCode: String): Int = languageCodesList.indexOf(languageCode) @@ -128,17 +128,17 @@ class LanguagesAdapter constructor( override fun performFiltering(constraint: CharSequence?): FilterResults { val filterResults = FilterResults() val temp: LinkedHashMap = LinkedHashMap() - if (constraint != null && language.localizedNames != null) { - val length: Int = language.localizedNames.size + if (constraint != null) { + val length: Int = language.getLocalizedNames().size var i = 0 while (i < length) { - val key: String = language.codes[i] - val value: String = language.localizedNames[i] + val key: String = language.getCodes()[i] + val value: String = language.getLocalizedNames()[i] val defaultlanguagecode = getIndexOfUserDefaultLocale(context) if (value.contains(constraint, true) || Locale(key) .getDisplayName( - Locale(language.codes[defaultlanguagecode]), + Locale(language.getCodes()[defaultlanguagecode]), ).contains(constraint, true) ) { temp[key] = value diff --git a/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt index d9ef4d6e8d..3b208b5c1b 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/LatLngTests.kt @@ -62,5 +62,5 @@ class LatLngTests { private fun assertPrettyCoordinateString( expected: String, place: LatLng, - ) = assertEquals(expected, place.prettyCoordinateString) + ) = assertEquals(expected, place.getPrettyCoordinateString()) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt index ea1d3402d3..9f73d2b81d 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/media/MediaDetailFragmentUnitTests.kt @@ -248,11 +248,11 @@ class MediaDetailFragmentUnitTests { @Throws(Exception::class) fun testOnUpdateCoordinatesClickedCurrentLocationNull() { `when`(media.coordinates).thenReturn(null) - `when`(locationManager.lastLocation).thenReturn(null) + `when`(locationManager.getLastLocation()).thenReturn(null) `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") fragment.onUpdateCoordinatesClicked() Mockito.verify(media, Mockito.times(1)).coordinates - Mockito.verify(locationManager, Mockito.times(1)).lastLocation + Mockito.verify(locationManager, Mockito.times(1)).getLastLocation() val shadowActivity: ShadowActivity = shadowOf(activity) val startedIntent = shadowActivity.nextStartedActivity val shadowIntent: ShadowIntent = shadowOf(startedIntent) @@ -276,11 +276,11 @@ class MediaDetailFragmentUnitTests { @Throws(Exception::class) fun testOnUpdateCoordinatesClickedCurrentLocationNotNull() { `when`(media.coordinates).thenReturn(null) - `when`(locationManager.lastLocation).thenReturn(LatLng(-0.000001, -0.999999, 0f)) + `when`(locationManager.getLastLocation()).thenReturn(LatLng(-0.000001, -0.999999, 0f)) `when`(applicationKvStore.getString(lastLocation)).thenReturn("37.773972,-122.431297") fragment.onUpdateCoordinatesClicked() - Mockito.verify(locationManager, Mockito.times(3)).lastLocation + Mockito.verify(locationManager, Mockito.times(3)).getLastLocation() val shadowActivity: ShadowActivity = shadowOf(activity) val startedIntent = shadowActivity.nextStartedActivity val shadowIntent: ShadowIntent = shadowOf(startedIntent) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt index 801d4e9005..f272a8288f 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/LanguagesAdapterTest.kt @@ -54,8 +54,8 @@ class LanguagesAdapterTest { .from(context) .inflate(R.layout.row_item_languages_spinner, null) as View - languageNamesList = language.localizedNames - languageCodesList = language.codes + languageNamesList = language.getLocalizedNames() + languageCodesList = language.getCodes() languagesAdapter = LanguagesAdapter(context, selectedLanguages) } @@ -124,12 +124,12 @@ class LanguagesAdapterTest { var i = 0 var s = 0 while (i < length) { - val key: String = language.codes[i] - val value: String = language.localizedNames[i] + val key: String = language.getCodes()[i] + val value: String = language.getLocalizedNames()[i] if (value.contains(constraint, true) || Locale(key) .getDisplayName( - Locale(language.codes[defaultlanguagecode!!]), + Locale(language.getCodes()[defaultlanguagecode!!]), ).contains(constraint, true) ) { s++ From 33548fa57d9d56c5208c39aeabd40b0fc2b336ad Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Tue, 3 Dec 2024 00:47:25 -0600 Subject: [PATCH 047/231] Convert profile package to kotlin (#5979) * Convert ViewModelFactory to kotlin * Convert UpdateAvatarResponse and related test to Kotlin * Convert LeaderboardResponse and related test to kotlin * Convert LeaderboardListAdapter to kotlin * Convert UserDetailAdapter to kotlin * Convert LeaderboardListViewModel to kotlin * Convert DataSourceClass to kotlin * Convert the LeaderboardFragment to kotlin * Converted AchievementsFragment to kotlin * Revert "Converted AchievementsFragment to kotlin" This reverts commit 4fcbb81e5dd95c1eab5910cab3d728959ad569f0. --------- Co-authored-by: Nicolas Raoul --- .../profile/leaderboard/DataSourceClass.java | 125 ------ .../profile/leaderboard/DataSourceClass.kt | 79 ++++ .../leaderboard/DataSourceFactory.java | 110 ------ .../profile/leaderboard/DataSourceFactory.kt | 27 ++ .../leaderboard/LeaderboardConstants.java | 45 --- .../leaderboard/LeaderboardConstants.kt | 44 +++ .../leaderboard/LeaderboardFragment.java | 363 ------------------ .../leaderboard/LeaderboardFragment.kt | 319 +++++++++++++++ .../profile/leaderboard/LeaderboardList.java | 137 ------- .../profile/leaderboard/LeaderboardList.kt | 61 +++ .../leaderboard/LeaderboardListAdapter.java | 93 ----- .../leaderboard/LeaderboardListAdapter.kt | 64 +++ .../leaderboard/LeaderboardListViewModel.java | 107 ------ .../leaderboard/LeaderboardListViewModel.kt | 54 +++ .../leaderboard/LeaderboardResponse.java | 237 ------------ .../leaderboard/LeaderboardResponse.kt | 19 + .../leaderboard/UpdateAvatarResponse.java | 77 ---- .../leaderboard/UpdateAvatarResponse.kt | 10 + .../leaderboard/UserDetailAdapter.java | 126 ------ .../profile/leaderboard/UserDetailAdapter.kt | 91 +++++ .../profile/leaderboard/ViewModelFactory.java | 41 -- .../profile/leaderboard/ViewModelFactory.kt | 26 ++ .../leaderboard/LeaderboardApiTest.java | 116 ------ .../commons/leaderboard/LeaderboardApiTest.kt | 121 ++++++ .../leaderboard/UpdateAvatarApiTest.java | 117 ------ .../leaderboard/UpdateAvatarApiTest.kt | 127 ++++++ 26 files changed, 1042 insertions(+), 1694 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java deleted file mode 100644 index 409450d607..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.java +++ /dev/null @@ -1,125 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING; - -import androidx.annotation.NonNull; -import androidx.lifecycle.MutableLiveData; -import androidx.paging.PageKeyedDataSource; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; -import java.util.Objects; -import timber.log.Timber; - -/** - * This class will call the leaderboard API to get new list when the pagination is performed - */ -public class DataSourceClass extends PageKeyedDataSource { - - private OkHttpJsonApiClient okHttpJsonApiClient; - private SessionManager sessionManager; - private MutableLiveData progressLiveStatus; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private String duration; - private String category; - private int limit; - private int offset; - - /** - * Initialise the Data Source Class with API params - * @param okHttpJsonApiClient - * @param sessionManager - * @param duration - * @param category - * @param limit - * @param offset - */ - public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager, - String duration, String category, int limit, int offset) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.sessionManager = sessionManager; - this.duration = duration; - this.category = category; - this.limit = limit; - this.offset = offset; - progressLiveStatus = new MutableLiveData<>(); - } - - - /** - * @return the status of the list - */ - public MutableLiveData getProgressLiveStatus() { - return progressLiveStatus; - } - - /** - * Loads the initial set of data from API - * @param params - * @param callback - */ - @Override - public void loadInitial(@NonNull LoadInitialParams params, - @NonNull LoadInitialCallback callback) { - - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name, - duration, category, String.valueOf(limit), String.valueOf(offset)) - .doOnSubscribe(disposable -> { - compositeDisposable.add(disposable); - progressLiveStatus.postValue(LOADING); - }).subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - progressLiveStatus.postValue(LOADED); - callback.onResult(response.getLeaderboardList(), null, response.getLimit()); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - progressLiveStatus.postValue(LOADING); - } - )); - - } - - /** - * Loads any data before the inital page is loaded - * @param params - * @param callback - */ - @Override - public void loadBefore(@NonNull LoadParams params, - @NonNull LoadCallback callback) { - - } - - /** - * Loads the next set of data on scrolling with offset as the limit of the last set of data - * @param params - * @param callback - */ - @Override - public void loadAfter(@NonNull LoadParams params, - @NonNull LoadCallback callback) { - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name, - duration, category, String.valueOf(limit), String.valueOf(params.key)) - .doOnSubscribe(disposable -> { - compositeDisposable.add(disposable); - progressLiveStatus.postValue(LOADING); - }).subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - progressLiveStatus.postValue(LOADED); - callback.onResult(response.getLeaderboardList(), params.key + limit); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - progressLiveStatus.postValue(LOADING); - } - )); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt new file mode 100644 index 0000000000..a6fe747e52 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceClass.kt @@ -0,0 +1,79 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.accounts.Account +import androidx.lifecycle.MutableLiveData +import androidx.paging.PageKeyedDataSource +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable +import timber.log.Timber +import java.util.Objects + +/** + * This class will call the leaderboard API to get new list when the pagination is performed + */ +class DataSourceClass( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager, + private val duration: String?, + private val category: String?, + private val limit: Int, + private val offset: Int +) : PageKeyedDataSource() { + val progressLiveStatus: MutableLiveData = MutableLiveData() + private val compositeDisposable = CompositeDisposable() + + + override fun loadInitial( + params: LoadInitialParams, callback: LoadInitialCallback + ) { + compositeDisposable.add(okHttpJsonApiClient.getLeaderboard( + sessionManager.currentAccount?.name, + duration, + category, + limit.toString(), + offset.toString() + ).doOnSubscribe { disposable: Disposable? -> + compositeDisposable.add(disposable!!) + progressLiveStatus.postValue(LOADING) + }.subscribe({ response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + progressLiveStatus.postValue(LOADED) + callback.onResult(response.leaderboardList!!, null, response.limit) + } + }, { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + progressLiveStatus.postValue(LOADING) + })) + } + + override fun loadBefore( + params: LoadParams, callback: LoadCallback + ) = Unit + + override fun loadAfter( + params: LoadParams, callback: LoadCallback + ) { + compositeDisposable.add(okHttpJsonApiClient.getLeaderboard( + Objects.requireNonNull(sessionManager.currentAccount).name, + duration, + category, + limit.toString(), + params.key.toString() + ).doOnSubscribe { disposable: Disposable? -> + compositeDisposable.add(disposable!!) + progressLiveStatus.postValue(LOADING) + }.subscribe({ response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + progressLiveStatus.postValue(LOADED) + callback.onResult(response.leaderboardList!!, params.key + limit) + } + }, { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + progressLiveStatus.postValue(LOADING) + })) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java deleted file mode 100644 index b2965785a8..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.java +++ /dev/null @@ -1,110 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.lifecycle.MutableLiveData; -import androidx.paging.DataSource; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; - -/** - * This class will create a new instance of the data source class on pagination - */ -public class DataSourceFactory extends DataSource.Factory { - - private MutableLiveData liveData; - private OkHttpJsonApiClient okHttpJsonApiClient; - private CompositeDisposable compositeDisposable; - private SessionManager sessionManager; - private String duration; - private String category; - private int limit; - private int offset; - - /** - * Gets the current set leaderboard list duration - */ - public String getDuration() { - return duration; - } - - /** - * Sets the current set leaderboard duration with the new duration - */ - public void setDuration(final String duration) { - this.duration = duration; - } - - /** - * Gets the current set leaderboard list category - */ - public String getCategory() { - return category; - } - - /** - * Sets the current set leaderboard category with the new category - */ - public void setCategory(final String category) { - this.category = category; - } - - /** - * Gets the current set leaderboard list limit - */ - public int getLimit() { - return limit; - } - - /** - * Sets the current set leaderboard limit with the new limit - */ - public void setLimit(final int limit) { - this.limit = limit; - } - - /** - * Gets the current set leaderboard list offset - */ - public int getOffset() { - return offset; - } - - /** - * Sets the current set leaderboard offset with the new offset - */ - public void setOffset(final int offset) { - this.offset = offset; - } - - /** - * Constructor for DataSourceFactory class - * @param okHttpJsonApiClient client for OKhttp - * @param compositeDisposable composite disposable - * @param sessionManager sessionManager - */ - public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable, - SessionManager sessionManager) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.compositeDisposable = compositeDisposable; - this.sessionManager = sessionManager; - liveData = new MutableLiveData<>(); - } - - /** - * @return the live data - */ - public MutableLiveData getMutableLiveData() { - return liveData; - } - - /** - * Creates the new instance of data source class - * @return - */ - @Override - public DataSource create() { - DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager, duration, category, limit, offset); - liveData.postValue(dataSourceClass); - return dataSourceClass; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt new file mode 100644 index 0000000000..6e979d8c38 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/DataSourceFactory.kt @@ -0,0 +1,27 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.MutableLiveData +import androidx.paging.DataSource +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient + +/** + * This class will create a new instance of the data source class on pagination + */ +class DataSourceFactory( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager +) : DataSource.Factory() { + val mutableLiveData: MutableLiveData = MutableLiveData() + var duration: String? = null + var category: String? = null + var limit: Int = 0 + var offset: Int = 0 + + /** + * Creates the new instance of data source class + */ + override fun create(): DataSource = DataSourceClass( + okHttpJsonApiClient, sessionManager, duration, category, limit, offset + ).also { mutableLiveData.postValue(it) } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java deleted file mode 100644 index 800287f4fc..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.java +++ /dev/null @@ -1,45 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -/** - * This class contains the constant variables for leaderboard - */ -public class LeaderboardConstants { - - /** - * This is the size of the page i.e. number items to load in a batch when pagination is performed - */ - public static final int PAGE_SIZE = 100; - - /** - * This is the starting offset, we set it to 0 to start loading from rank 1 - */ - public static final int START_OFFSET = 0; - - /** - * This is the prefix of the user's homepage url, appending the username will give us complete url - */ - public static final String USER_LINK_PREFIX = "https://commons.wikimedia.org/wiki/User:"; - - /** - * This is the a constant string for the state loading, when the pages are getting loaded we can - * use this constant to identify if we need to show the progress bar or not - */ - public final static String LOADING = "Loading"; - - /** - * This is the a constant string for the state loaded, when the pages are loaded we can - * use this constant to identify if we need to show the progress bar or not - */ - public final static String LOADED = "Loaded"; - - /** - * This API endpoint is to update the leaderboard avatar - */ - public final static String UPDATE_AVATAR_END_POINT = "/update_avatar.py"; - - /** - * This API endpoint is to get leaderboard data - */ - public final static String LEADERBOARD_END_POINT = "/leaderboard.py"; - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt new file mode 100644 index 0000000000..bf8d45c5f2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardConstants.kt @@ -0,0 +1,44 @@ +package fr.free.nrw.commons.profile.leaderboard + +/** + * This class contains the constant variables for leaderboard + */ +object LeaderboardConstants { + /** + * This is the size of the page i.e. number items to load in a batch when pagination is performed + */ + const val PAGE_SIZE: Int = 100 + + /** + * This is the starting offset, we set it to 0 to start loading from rank 1 + */ + const val START_OFFSET: Int = 0 + + /** + * This is the prefix of the user's homepage url, appending the username will give us complete url + */ + const val USER_LINK_PREFIX: String = "https://commons.wikimedia.org/wiki/User:" + + sealed class LoadingStatus { + /** + * This is the state loading, when the pages are getting loaded we can + * use this constant to identify if we need to show the progress bar or not + */ + data object LOADING: LoadingStatus() + /** + * This is the state loaded, when the pages are loaded we can + * use this constant to identify if we need to show the progress bar or not + */ + data object LOADED: LoadingStatus() + } + + /** + * This API endpoint is to update the leaderboard avatar + */ + const val UPDATE_AVATAR_END_POINT: String = "/update_avatar.py" + + /** + * This API endpoint is to get leaderboard data + */ + const val LEADERBOARD_END_POINT: String = "/leaderboard.py" +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java deleted file mode 100644 index a9cc222eaf..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.java +++ /dev/null @@ -1,363 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET; - -import android.accounts.Account; -import android.content.Context; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ArrayAdapter; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.lifecycle.ViewModelProvider; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.MergeAdapter; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.profile.ProfileActivity; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Objects; -import javax.inject.Inject; -import timber.log.Timber; - -/** - * This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment - */ -public class LeaderboardFragment extends CommonsDaggerSupportFragment { - - - @Inject - SessionManager sessionManager; - - @Inject - OkHttpJsonApiClient okHttpJsonApiClient; - - @Inject - ViewModelFactory viewModelFactory; - - /** - * View model for the paged leaderboard list - */ - private LeaderboardListViewModel viewModel; - - /** - * Composite disposable for API call - */ - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - /** - * Duration of the leaderboard API - */ - private String duration; - - /** - * Category of the Leaderboard API - */ - private String category; - - /** - * Page size of the leaderboard API - */ - private int limit = PAGE_SIZE; - - /** - * offset for the leaderboard API - */ - private int offset = START_OFFSET; - - /** - * Set initial User Rank to 0 - */ - private int userRank; - - /** - * This variable represents if user wants to scroll to his rank or not - */ - private boolean scrollToRank; - - private String userName; - - private FragmentLeaderboardBinding binding; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - userName = getArguments().getString(ProfileActivity.KEY_USERNAME); - } - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentLeaderboardBinding.inflate(inflater, container, false); - - hideLayouts(); - - // Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu - if(ConfigUtils.isBetaFlavour()) { - binding.progressBar.setVisibility(View.GONE); - binding.scroll.setVisibility(View.GONE); - return binding.getRoot(); - } - - binding.progressBar.setVisibility(View.VISIBLE); - setSpinners(); - - /** - * This array is for the duration filter, we have three filters weekly, yearly and all-time - * each filter have a key and value pair, the value represents the param of the API - */ - String[] durationValues = getContext().getResources().getStringArray(R.array.leaderboard_duration_values); - - /** - * This array is for the category filter, we have three filters upload, used and nearby - * each filter have a key and value pair, the value represents the param of the API - */ - String[] categoryValues = getContext().getResources().getStringArray(R.array.leaderboard_category_values); - - duration = durationValues[0]; - category = categoryValues[0]; - - setLeaderboard(duration, category, limit, offset); - - binding.durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - - duration = durationValues[binding.durationSpinner.getSelectedItemPosition()]; - refreshLeaderboard(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - binding.categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() { - @Override - public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - category = categoryValues[binding.categorySpinner.getSelectedItemPosition()]; - refreshLeaderboard(); - } - - @Override - public void onNothingSelected(AdapterView adapterView) { - } - }); - - - binding.scroll.setOnClickListener(view -> scrollToUserRank()); - - - return binding.getRoot(); - } - - @Override - public void setMenuVisibility(boolean visible) { - super.setMenuVisibility(visible); - - // Whenever this fragment is revealed in a menu, - // notify Beta users the page data is unavailable - if(ConfigUtils.isBetaFlavour() && visible) { - Context ctx = null; - if(getContext() != null) { - ctx = getContext(); - } else if(getView() != null && getView().getContext() != null) { - ctx = getView().getContext(); - } - if(ctx != null) { - Toast.makeText(ctx, - R.string.leaderboard_unavailable_beta, - Toast.LENGTH_LONG).show(); - } - } - } - - /** - * Refreshes the leaderboard list - */ - private void refreshLeaderboard() { - scrollToRank = false; - if (viewModel != null) { - viewModel.refresh(duration, category, limit, offset); - setLeaderboard(duration, category, limit, offset); - } - } - - /** - * Performs Auto Scroll to the User's Rank - * We use userRank+1 to load one extra user and prevent overlapping of my rank button - * If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top - */ - private void scrollToUserRank() { - - if(userRank==0){ - Toast.makeText(getContext(),R.string.no_achievements_yet,Toast.LENGTH_SHORT).show(); - }else { - if (binding == null) { - return; - } - if (Objects.requireNonNull(binding.leaderboardList.getAdapter()).getItemCount() - > userRank + 1) { - binding.leaderboardList.smoothScrollToPosition(userRank + 1); - } else { - if (viewModel != null) { - viewModel.refresh(duration, category, userRank + 1, 0); - setLeaderboard(duration, category, userRank + 1, 0); - scrollToRank = true; - } - } - } - - } - - /** - * Set the spinners for the leaderboard filters - */ - private void setSpinners() { - ArrayAdapter categoryAdapter = ArrayAdapter.createFromResource(getContext(), - R.array.leaderboard_categories, android.R.layout.simple_spinner_item); - categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.categorySpinner.setAdapter(categoryAdapter); - - ArrayAdapter durationAdapter = ArrayAdapter.createFromResource(getContext(), - R.array.leaderboard_durations, android.R.layout.simple_spinner_item); - durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - binding.durationSpinner.setAdapter(durationAdapter); - } - - /** - * To call the API to get results - * which then sets the views using setLeaderboardUser method - */ - private void setLeaderboard(String duration, String category, int limit, int offset) { - if (checkAccount()) { - try { - compositeDisposable.add(okHttpJsonApiClient - .getLeaderboard(Objects.requireNonNull(userName), - duration, category, null, null) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null && response.getStatus() == 200) { - userRank = response.getRank(); - setViews(response, duration, category, limit, offset); - } - }, - t -> { - Timber.e(t, "Fetching leaderboard statistics failed"); - onError(); - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - } - } - } - - /** - * Set the views - * @param response Leaderboard Response Object - */ - private void setViews(LeaderboardResponse response, String duration, String category, int limit, int offset) { - viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class); - viewModel.setParams(duration, category, limit, offset); - LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter(); - UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response); - MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter); - LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext()); - binding.leaderboardList.setLayoutManager(linearLayoutManager); - binding.leaderboardList.setAdapter(mergeAdapter); - viewModel.getListLiveData().observe(getViewLifecycleOwner(), leaderboardListAdapter::submitList); - viewModel.getProgressLoadStatus().observe(getViewLifecycleOwner(), status -> { - if (Objects.requireNonNull(status).equalsIgnoreCase(LOADING)) { - showProgressBar(); - } else if (status.equalsIgnoreCase(LOADED)) { - hideProgressBar(); - if (scrollToRank) { - binding.leaderboardList.smoothScrollToPosition(userRank + 1); - } - } - }); - } - - /** - * to hide progressbar - */ - private void hideProgressBar() { - if (binding != null) { - binding.progressBar.setVisibility(View.GONE); - binding.categorySpinner.setVisibility(View.VISIBLE); - binding.durationSpinner.setVisibility(View.VISIBLE); - binding.scroll.setVisibility(View.VISIBLE); - binding.leaderboardList.setVisibility(View.VISIBLE); - } - } - - /** - * to show progressbar - */ - private void showProgressBar() { - if (binding != null) { - binding.progressBar.setVisibility(View.VISIBLE); - binding.scroll.setVisibility(View.INVISIBLE); - } - } - - /** - * used to hide the layouts while fetching results from api - */ - private void hideLayouts(){ - binding.categorySpinner.setVisibility(View.INVISIBLE); - binding.durationSpinner.setVisibility(View.INVISIBLE); - binding.leaderboardList.setVisibility(View.INVISIBLE); - } - - /** - * check to ensure that user is logged in - * @return - */ - private boolean checkAccount(){ - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(getActivity()); - return false; - } - return true; - } - - /** - * Shows a generic error toast when error occurs while loading leaderboard - */ - private void onError() { - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); - if (binding!=null) { - binding.progressBar.setVisibility(View.GONE); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - compositeDisposable.clear(); - binding = null; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt new file mode 100644 index 0000000000..e77c24c8d9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardFragment.kt @@ -0,0 +1,319 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.content.Context +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Toast +import androidx.lifecycle.ViewModelProvider +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.MergeAdapter +import fr.free.nrw.commons.R +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.databinding.FragmentLeaderboardBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADED +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus.LOADING +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.Objects +import javax.inject.Inject + +/** + * This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment + */ +class LeaderboardFragment : CommonsDaggerSupportFragment() { + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private var viewModel: LeaderboardListViewModel? = null + private var duration: String? = null + private var category: String? = null + private val limit: Int = PAGE_SIZE + private val offset: Int = START_OFFSET + private var userRank = 0 + private var scrollToRank = false + private var userName: String? = null + private var binding: FragmentLeaderboardBinding? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { userName = it.getString(ProfileActivity.KEY_USERNAME) } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + binding = FragmentLeaderboardBinding.inflate(inflater, container, false) + + hideLayouts() + + // Leaderboard currently unimplemented in Beta flavor. Skip all API calls and disable menu + if (isBetaFlavour) { + binding!!.progressBar.visibility = View.GONE + binding!!.scroll.visibility = View.GONE + return binding!!.root + } + + binding!!.progressBar.visibility = View.VISIBLE + setSpinners() + + /* + * This array is for the duration filter, we have three filters weekly, yearly and all-time + * each filter have a key and value pair, the value represents the param of the API + */ + val durationValues = requireContext().resources + .getStringArray(R.array.leaderboard_duration_values) + duration = durationValues[0] + + /* + * This array is for the category filter, we have three filters upload, used and nearby + * each filter have a key and value pair, the value represents the param of the API + */ + val categoryValues = requireContext().resources + .getStringArray(R.array.leaderboard_category_values) + category = categoryValues[0] + + setLeaderboard(duration, category, limit, offset) + + with(binding!!) { + durationSpinner.onItemSelectedListener = SelectionListener { + duration = durationValues[durationSpinner.selectedItemPosition] + refreshLeaderboard() + } + + categorySpinner.onItemSelectedListener = SelectionListener { + category = categoryValues[categorySpinner.selectedItemPosition] + refreshLeaderboard() + } + + scroll.setOnClickListener { scrollToUserRank() } + + return root + } + } + + override fun setMenuVisibility(visible: Boolean) { + super.setMenuVisibility(visible) + + // Whenever this fragment is revealed in a menu, + // notify Beta users the page data is unavailable + if (isBetaFlavour && visible) { + val ctx: Context? = if (context != null) { + context + } else if (view != null && requireView().context != null) { + requireView().context + } else { + null + } + + ctx?.let { + Toast.makeText(it, R.string.leaderboard_unavailable_beta, Toast.LENGTH_LONG).show() + } + } + } + + /** + * Refreshes the leaderboard list + */ + private fun refreshLeaderboard() { + scrollToRank = false + viewModel?.let { + it.refresh(duration, category, limit, offset) + setLeaderboard(duration, category, limit, offset) + } + } + + /** + * Performs Auto Scroll to the User's Rank + * We use userRank+1 to load one extra user and prevent overlapping of my rank button + * If you are viewing the leaderboard below userRank, it scrolls to the user rank at the top + */ + private fun scrollToUserRank() { + if (userRank == 0) { + Toast.makeText(context, R.string.no_achievements_yet, Toast.LENGTH_SHORT).show() + } else { + if (binding == null) { + return + } + val itemCount = binding?.leaderboardList?.adapter?.itemCount ?: 0 + if (itemCount > userRank + 1) { + binding!!.leaderboardList.smoothScrollToPosition(userRank + 1) + } else { + viewModel?.let { + it.refresh(duration, category, userRank + 1, 0) + setLeaderboard(duration, category, userRank + 1, 0) + scrollToRank = true + } + } + } + } + + /** + * Set the spinners for the leaderboard filters + */ + private fun setSpinners() { + val categoryAdapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.leaderboard_categories, android.R.layout.simple_spinner_item + ) + categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding!!.categorySpinner.adapter = categoryAdapter + + val durationAdapter = ArrayAdapter.createFromResource( + requireContext(), + R.array.leaderboard_durations, android.R.layout.simple_spinner_item + ) + durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + binding!!.durationSpinner.adapter = durationAdapter + } + + /** + * To call the API to get results + * which then sets the views using setLeaderboardUser method + */ + private fun setLeaderboard(duration: String?, category: String?, limit: Int, offset: Int) { + if (checkAccount()) { + try { + compositeDisposable.add( + okHttpJsonApiClient.getLeaderboard( + Objects.requireNonNull(userName), + duration, category, null, null + ) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response: LeaderboardResponse? -> + if (response != null && response.status == 200) { + userRank = response.rank!! + setViews(response, duration, category, limit, offset) + } + }, + { t: Throwable? -> + Timber.e(t, "Fetching leaderboard statistics failed") + onError() + } + )) + } catch (e: Exception) { + Timber.d(e, "success") + } + } + } + + /** + * Set the views + * @param response Leaderboard Response Object + */ + private fun setViews( + response: LeaderboardResponse, + duration: String?, + category: String?, + limit: Int, + offset: Int + ) { + viewModel = ViewModelProvider(this, viewModelFactory).get( + LeaderboardListViewModel::class.java + ) + viewModel!!.setParams(duration, category, limit, offset) + val leaderboardListAdapter = LeaderboardListAdapter() + val userDetailAdapter = UserDetailAdapter(response) + val mergeAdapter = MergeAdapter(userDetailAdapter, leaderboardListAdapter) + val linearLayoutManager = LinearLayoutManager(context) + binding!!.leaderboardList.layoutManager = linearLayoutManager + binding!!.leaderboardList.adapter = mergeAdapter + viewModel!!.listLiveData.observe(viewLifecycleOwner, leaderboardListAdapter::submitList) + + viewModel!!.progressLoadStatus.observe(viewLifecycleOwner) { status -> + when (status) { + LOADING -> { + showProgressBar() + } + LOADED -> { + hideProgressBar() + if (scrollToRank) { + binding!!.leaderboardList.smoothScrollToPosition(userRank + 1) + } + } + } + } + } + + /** + * to hide progressbar + */ + private fun hideProgressBar() = binding?.let { + it.progressBar.visibility = View.GONE + it.categorySpinner.visibility = View.VISIBLE + it.durationSpinner.visibility = View.VISIBLE + it.scroll.visibility = View.VISIBLE + it.leaderboardList.visibility = View.VISIBLE + } + + /** + * to show progressbar + */ + private fun showProgressBar() = binding?.let { + it.progressBar.visibility = View.VISIBLE + it.scroll.visibility = View.INVISIBLE + } + + /** + * used to hide the layouts while fetching results from api + */ + private fun hideLayouts() = binding?.let { + it.categorySpinner.visibility = View.INVISIBLE + it.durationSpinner.visibility = View.INVISIBLE + it.leaderboardList.visibility = View.INVISIBLE + } + + /** + * check to ensure that user is logged in + */ + private fun checkAccount() = if (sessionManager.currentAccount == null) { + Timber.d("Current account is null") + showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) + sessionManager.forceLogin(requireActivity()) + false + } else { + true + } + + /** + * Shows a generic error toast when error occurs while loading leaderboard + */ + private fun onError() { + showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) + binding?.let { it.progressBar.visibility = View.GONE } + } + + override fun onDestroy() { + super.onDestroy() + compositeDisposable.clear() + binding = null + } + + private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener { + override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) = + handler() + + override fun onNothingSelected(p0: AdapterView<*>?) = Unit + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java deleted file mode 100644 index 5558f3d9eb..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.java +++ /dev/null @@ -1,137 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.DiffUtil; -import androidx.recyclerview.widget.DiffUtil.ItemCallback; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * This class represents the leaderboard API response sub part of i.e. leaderboard list - * The leaderboard list will contain the ranking of the users from 1 to n, - * avatars, username and count in the selected category. - */ -public class LeaderboardList { - - /** - * Username of the user - * Example value - Syced - */ - @SerializedName("username") - @Expose - private String username; - - /** - * Count in the category - * Example value - 10 - */ - @SerializedName("category_count") - @Expose - private Integer categoryCount; - - /** - * URL of the avatar of user - * Example value = https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png - */ - @SerializedName("avatar") - @Expose - private String avatar; - - /** - * Rank of the user - * Example value - 1 - */ - @SerializedName("rank") - @Expose - private Integer rank; - - /** - * @return the username of the user in the leaderboard list - */ - public String getUsername() { - return username; - } - - /** - * Sets the username of the user in the leaderboard list - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the category count of the user in the leaderboard list - */ - public Integer getCategoryCount() { - return categoryCount; - } - - /** - * Sets the category count of the user in the leaderboard list - */ - public void setCategoryCount(Integer categoryCount) { - this.categoryCount = categoryCount; - } - - /** - * @return the avatar of the user in the leaderboard list - */ - public String getAvatar() { - return avatar; - } - - /** - * Sets the avatar of the user in the leaderboard list - */ - public void setAvatar(String avatar) { - this.avatar = avatar; - } - - /** - * @return the rank of the user in the leaderboard list - */ - public Integer getRank() { - return rank; - } - - /** - * Sets the rank of the user in the leaderboard list - */ - public void setRank(Integer rank) { - this.rank = rank; - } - - - /** - * This method checks for the diff in the callbacks for paged lists - */ - public static DiffUtil.ItemCallback DIFF_CALLBACK = - new ItemCallback() { - @Override - public boolean areItemsTheSame(@NonNull LeaderboardList oldItem, - @NonNull LeaderboardList newItem) { - return newItem == oldItem; - } - - @Override - public boolean areContentsTheSame(@NonNull LeaderboardList oldItem, - @NonNull LeaderboardList newItem) { - return newItem.getRank().equals(oldItem.getRank()); - } - }; - - /** - * Returns true if two objects are equal, false otherwise - * @param obj - * @return - */ - @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; - } - - LeaderboardList leaderboardList = (LeaderboardList) obj; - return leaderboardList.getRank().equals(this.getRank()); - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt new file mode 100644 index 0000000000..dc6d93e15a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardList.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.recyclerview.widget.DiffUtil +import com.google.gson.annotations.SerializedName + +/** + * This class represents the leaderboard API response sub part of i.e. leaderboard list + * The leaderboard list will contain the ranking of the users from 1 to n, + * avatars, username and count in the selected category. + */ +data class LeaderboardList ( + @SerializedName("username") + var username: String? = null, + @SerializedName("category_count") + var categoryCount: Int? = null, + @SerializedName("avatar") + var avatar: String? = null, + @SerializedName("rank") + var rank: Int? = null +) { + + /** + * Returns true if two objects are equal, false otherwise + * @param other + * @return + */ + override fun equals(other: Any?): Boolean { + if (other === this) { + return true + } + + val leaderboardList = other as LeaderboardList + return leaderboardList.rank == rank + } + + override fun hashCode(): Int { + var result = username?.hashCode() ?: 0 + result = 31 * result + (categoryCount ?: 0) + result = 31 * result + (avatar?.hashCode() ?: 0) + result = 31 * result + (rank ?: 0) + return result + } + + companion object { + /** + * This method checks for the diff in the callbacks for paged lists + */ + var DIFF_CALLBACK: DiffUtil.ItemCallback = + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: LeaderboardList, + newItem: LeaderboardList + ): Boolean = newItem === oldItem + + override fun areContentsTheSame( + oldItem: LeaderboardList, + newItem: LeaderboardList + ): Boolean = newItem.rank == oldItem.rank + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java deleted file mode 100644 index 9af24159af..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.java +++ /dev/null @@ -1,93 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - - -import android.app.Activity; -import android.content.Context; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import androidx.annotation.NonNull; -import androidx.paging.PagedListAdapter; -import androidx.recyclerview.widget.RecyclerView; -import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.profile.ProfileActivity; - -/** - * This class extends RecyclerView.Adapter and creates the List section of the leaderboard - */ -public class LeaderboardListAdapter extends PagedListAdapter { - - public LeaderboardListAdapter() { - super(LeaderboardList.DIFF_CALLBACK); - } - - public class ListViewHolder extends RecyclerView.ViewHolder { - TextView rank; - SimpleDraweeView avatar; - TextView username; - TextView count; - - public ListViewHolder(View itemView) { - super(itemView); - this.rank = itemView.findViewById(R.id.user_rank); - this.avatar = itemView.findViewById(R.id.user_avatar); - this.username = itemView.findViewById(R.id.user_name); - this.count = itemView.findViewById(R.id.user_count); - } - - /** - * This method will return the Context - * @return Context - */ - public Context getContext() { - return itemView.getContext(); - } - } - - /** - * Overrides the onCreateViewHolder and inflates the recyclerview list item layout - * @param parent - * @param viewType - * @return - */ - @NonNull - @Override - public LeaderboardListAdapter.ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.leaderboard_list_element, parent, false); - - return new ListViewHolder(view); - } - - /** - * Overrides the onBindViewHolder Set the view at the specific position with the specific value - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull LeaderboardListAdapter.ListViewHolder holder, int position) { - TextView rank = holder.rank; - SimpleDraweeView avatar = holder.avatar; - TextView username = holder.username; - TextView count = holder.count; - - rank.setText(getItem(position).getRank().toString()); - - avatar.setImageURI(Uri.parse(getItem(position).getAvatar())); - username.setText(getItem(position).getUsername()); - count.setText(getItem(position).getCategoryCount().toString()); - - /* - Now that we have our in app profile-section, lets take the user there - */ - holder.itemView.setOnClickListener(view -> { - if (view.getContext() instanceof ProfileActivity) { - ((Activity) (view.getContext())).finish(); - } - ProfileActivity.startYourself(view.getContext(), getItem(position).getUsername(), true); - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt new file mode 100644 index 0000000000..c7bccf950c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListAdapter.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.app.Activity +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.view.SimpleDraweeView +import fr.free.nrw.commons.R +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.leaderboard.LeaderboardList.Companion.DIFF_CALLBACK +import fr.free.nrw.commons.profile.leaderboard.LeaderboardListAdapter.ListViewHolder + + +/** + * This class extends RecyclerView.Adapter and creates the List section of the leaderboard + */ +class LeaderboardListAdapter : PagedListAdapter(DIFF_CALLBACK) { + inner class ListViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var rank: TextView? = itemView.findViewById(R.id.user_rank) + var avatar: SimpleDraweeView? = itemView.findViewById(R.id.user_avatar) + var username: TextView? = itemView.findViewById(R.id.user_name) + var count: TextView? = itemView.findViewById(R.id.user_count) + } + + /** + * Overrides the onCreateViewHolder and inflates the recyclerview list item layout + * @param parent + * @param viewType + * @return + */ + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListViewHolder = + ListViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.leaderboard_list_element, parent, false) + ) + + /** + * Overrides the onBindViewHolder Set the view at the specific position with the specific value + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: ListViewHolder, position: Int) = with (holder) { + val item = getItem(position)!! + + rank?.text = item.rank.toString() + avatar?.setImageURI(Uri.parse(item.avatar)) + username?.text = item.username + count?.text = item.categoryCount.toString() + + /* + Now that we have our in app profile-section, lets take the user there + */ + itemView.setOnClickListener { view: View -> + if (view.context is ProfileActivity) { + ((view.context) as Activity).finish() + } + ProfileActivity.startYourself(view.context, item.username, true) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java deleted file mode 100644 index 909b4f6465..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.java +++ /dev/null @@ -1,107 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE; - -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; -import androidx.lifecycle.ViewModel; -import androidx.paging.LivePagedListBuilder; -import androidx.paging.PagedList; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import io.reactivex.disposables.CompositeDisposable; - -/** - * Extends the ViewModel class and creates the LeaderboardList View Model - */ -public class LeaderboardListViewModel extends ViewModel { - - private DataSourceFactory dataSourceFactory; - private LiveData> listLiveData; - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - private LiveData progressLoadStatus = new MutableLiveData<>(); - - /** - * Constructor for a new LeaderboardListViewModel - * @param okHttpJsonApiClient - * @param sessionManager - */ - public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager - sessionManager) { - - dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient, - compositeDisposable, sessionManager); - initializePaging(); - } - - - /** - * Initialises the paging - */ - private void initializePaging() { - - PagedList.Config pagedListConfig = - new PagedList.Config.Builder() - .setEnablePlaceholders(false) - .setInitialLoadSizeHint(PAGE_SIZE) - .setPageSize(PAGE_SIZE).build(); - - listLiveData = new LivePagedListBuilder<>(dataSourceFactory, pagedListConfig) - .build(); - - progressLoadStatus = Transformations - .switchMap(dataSourceFactory.getMutableLiveData(), DataSourceClass::getProgressLiveStatus); - - } - - /** - * Refreshes the paged list with the new params and starts the loading of new data - * @param duration - * @param category - * @param limit - * @param offset - */ - public void refresh(String duration, String category, int limit, int offset) { - dataSourceFactory.setDuration(duration); - dataSourceFactory.setCategory(category); - dataSourceFactory.setLimit(limit); - dataSourceFactory.setOffset(offset); - dataSourceFactory.getMutableLiveData().getValue().invalidate(); - } - - /** - * Sets the new params for the paged list API calls - * @param duration - * @param category - * @param limit - * @param offset - */ - public void setParams(String duration, String category, int limit, int offset) { - dataSourceFactory.setDuration(duration); - dataSourceFactory.setCategory(category); - dataSourceFactory.setLimit(limit); - dataSourceFactory.setOffset(offset); - } - - /** - * @return the loading status of paged list - */ - public LiveData getProgressLoadStatus() { - return progressLoadStatus; - } - - /** - * @return the paged list with live data - */ - public LiveData> getListLiveData() { - return listLiveData; - } - - @Override - protected void onCleared() { - super.onCleared(); - compositeDisposable.clear(); - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt new file mode 100644 index 0000000000..7d649b67b0 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardListViewModel.kt @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.map +import androidx.lifecycle.switchMap +import androidx.paging.LivePagedListBuilder +import androidx.paging.PagedList +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LoadingStatus +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE + +/** + * Extends the ViewModel class and creates the LeaderboardList View Model + */ +class LeaderboardListViewModel( + okHttpJsonApiClient: OkHttpJsonApiClient, + sessionManager: SessionManager +) : ViewModel() { + private val dataSourceFactory = DataSourceFactory(okHttpJsonApiClient, sessionManager) + + val listLiveData: LiveData> = LivePagedListBuilder( + dataSourceFactory, + PagedList.Config.Builder() + .setEnablePlaceholders(false) + .setInitialLoadSizeHint(PAGE_SIZE) + .setPageSize(PAGE_SIZE).build() + ).build() + + val progressLoadStatus: LiveData = + dataSourceFactory.mutableLiveData.switchMap { it.progressLiveStatus } + + /** + * Refreshes the paged list with the new params and starts the loading of new data + */ + fun refresh(duration: String?, category: String?, limit: Int, offset: Int) { + dataSourceFactory.duration = duration + dataSourceFactory.category = category + dataSourceFactory.limit = limit + dataSourceFactory.offset = offset + dataSourceFactory.mutableLiveData.value!!.invalidate() + } + + /** + * Sets the new params for the paged list API calls + */ + fun setParams(duration: String?, category: String?, limit: Int, offset: Int) { + dataSourceFactory.duration = duration + dataSourceFactory.category = category + dataSourceFactory.limit = limit + dataSourceFactory.offset = offset + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java deleted file mode 100644 index 34294fca9c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.java +++ /dev/null @@ -1,237 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import java.util.List; -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * GSON Response Class for Leaderboard API response - */ -public class LeaderboardResponse { - - /** - * Status Code returned from the API - * Example value - 200 - */ - @SerializedName("status") - @Expose - private Integer status; - - /** - * Username returned from the API - * Example value - Syced - */ - @SerializedName("username") - @Expose - private String username; - - /** - * Category count returned from the API - * Example value - 10 - */ - @SerializedName("category_count") - @Expose - private Integer categoryCount; - - /** - * Limit returned from the API - * Example value - 10 - */ - @SerializedName("limit") - @Expose - private int limit; - - /** - * Avatar returned from the API - * Example value - https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png - */ - @SerializedName("avatar") - @Expose - private String avatar; - - /** - * Offset returned from the API - * Example value - 0 - */ - @SerializedName("offset") - @Expose - private int offset; - - /** - * Duration returned from the API - * Example value - yearly - */ - @SerializedName("duration") - @Expose - private String duration; - - /** - * Leaderboard list returned from the API - * Example value - [{ - * "username": "Fæ", - * "category_count": 107147, - * "avatar": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png", - * "rank": 1 - * }] - */ - @SerializedName("leaderboard_list") - @Expose - private List leaderboardList = null; - - /** - * Category returned from the API - * Example value - upload - */ - @SerializedName("category") - @Expose - private String category; - - /** - * Rank returned from the API - * Example value - 1 - */ - @SerializedName("rank") - @Expose - private Integer rank; - - /** - * @return the status code - */ - public Integer getStatus() { - return status; - } - - /** - * Sets the status code - */ - public void setStatus(Integer status) { - this.status = status; - } - - /** - * @return the username - */ - public String getUsername() { - return username; - } - - /** - * Sets the username - */ - public void setUsername(String username) { - this.username = username; - } - - /** - * @return the category count - */ - public Integer getCategoryCount() { - return categoryCount; - } - - /** - * Sets the category count - */ - public void setCategoryCount(Integer categoryCount) { - this.categoryCount = categoryCount; - } - - /** - * @return the limit - */ - public int getLimit() { - return limit; - } - - /** - * Sets the limit - */ - public void setLimit(int limit) { - this.limit = limit; - } - - /** - * @return the avatar - */ - public String getAvatar() { - return avatar; - } - - /** - * Sets the avatar - */ - public void setAvatar(String avatar) { - this.avatar = avatar; - } - - /** - * @return the offset - */ - public int getOffset() { - return offset; - } - - /** - * Sets the offset - */ - public void setOffset(int offset) { - this.offset = offset; - } - - /** - * @return the duration - */ - public String getDuration() { - return duration; - } - - /** - * Sets the duration - */ - public void setDuration(String duration) { - this.duration = duration; - } - - /** - * @return the leaderboard list - */ - public List getLeaderboardList() { - return leaderboardList; - } - - /** - * Sets the leaderboard list - */ - public void setLeaderboardList(List leaderboardList) { - this.leaderboardList = leaderboardList; - } - - /** - * @return the category - */ - public String getCategory() { - return category; - } - - /** - * Sets the category - */ - public void setCategory(String category) { - this.category = category; - } - - /** - * @return the rank - */ - public Integer getRank() { - return rank; - } - - /** - * Sets the rank - */ - public void setRank(Integer rank) { - this.rank = rank; - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt new file mode 100644 index 0000000000..8be3426509 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/LeaderboardResponse.kt @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.profile.leaderboard + +import com.google.gson.annotations.SerializedName + +/** + * GSON Response Class for Leaderboard API response + */ +data class LeaderboardResponse( + @SerializedName("status") var status: Int? = null, + @SerializedName("username") var username: String? = null, + @SerializedName("category_count") var categoryCount: Int? = null, + @SerializedName("limit") var limit: Int = 0, + @SerializedName("avatar") var avatar: String? = null, + @SerializedName("offset") var offset: Int = 0, + @SerializedName("duration") var duration: String? = null, + @SerializedName("leaderboard_list") var leaderboardList: List? = null, + @SerializedName("category") var category: String? = null, + @SerializedName("rank") var rank: Int? = null +) \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java deleted file mode 100644 index 15449a4885..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.java +++ /dev/null @@ -1,77 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import com.google.gson.annotations.Expose; -import com.google.gson.annotations.SerializedName; - -/** - * GSON Response Class for Update Avatar API response - */ -public class UpdateAvatarResponse { - - /** - * Status Code returned from the API - * Example value - 200 - */ - @SerializedName("status") - @Expose - private String status; - - /** - * Message returned from the API - * Example value - Avatar Updated - */ - @SerializedName("message") - @Expose - private String message; - - /** - * Username returned from the API - * Example value - Syced - */ - @SerializedName("user") - @Expose - private String user; - - /** - * @return the status code - */ - public String getStatus() { - return status; - } - - /** - * Sets the status code - */ - public void setStatus(String status) { - this.status = status; - } - - /** - * @return the message - */ - public String getMessage() { - return message; - } - - /** - * Sets the message - */ - public void setMessage(String message) { - this.message = message; - } - - /** - * @return the username - */ - public String getUser() { - return user; - } - - /** - * Sets the username - */ - public void setUser(String user) { - this.user = user; - } - -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt new file mode 100644 index 0000000000..75fb8f2681 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UpdateAvatarResponse.kt @@ -0,0 +1,10 @@ +package fr.free.nrw.commons.profile.leaderboard + +/** + * GSON Response Class for Update Avatar API response + */ +data class UpdateAvatarResponse( + var status: String? = null, + var message: String? = null, + var user: String? = null +) \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java deleted file mode 100644 index 75b9de938c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.java +++ /dev/null @@ -1,126 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.net.Uri; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; -import android.widget.Toast; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import com.facebook.drawee.view.SimpleDraweeView; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.R; - - -/** - * This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard - */ -public class UserDetailAdapter extends RecyclerView.Adapter { - - private LeaderboardResponse leaderboardResponse; - - /** - * Stores the username of currently logged in user. - */ - private String currentlyLoggedInUserName = null; - - public UserDetailAdapter(LeaderboardResponse leaderboardResponse) { - this.leaderboardResponse = leaderboardResponse; - } - - public class DataViewHolder extends RecyclerView.ViewHolder { - - private TextView rank; - private SimpleDraweeView avatar; - private TextView username; - private TextView count; - - public DataViewHolder(@NonNull View itemView) { - super(itemView); - this.rank = itemView.findViewById(R.id.rank); - this.avatar = itemView.findViewById(R.id.avatar); - this.username = itemView.findViewById(R.id.username); - this.count = itemView.findViewById(R.id.count); - } - - /** - * This method will return the Context - * @return Context - */ - public Context getContext() { - return itemView.getContext(); - } - } - - /** - * Overrides the onCreateViewHolder and sets the view with leaderboard user element layout - * @param parent - * @param viewType - * @return - */ - @NonNull - @Override - public UserDetailAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent, - int viewType) { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.leaderboard_user_element, parent, false); - return new DataViewHolder(view); - } - - /** - * Overrides the onBindViewHolder Set the view at the specific position with the specific value - * @param holder - * @param position - */ - @Override - public void onBindViewHolder(@NonNull UserDetailAdapter.DataViewHolder holder, int position) { - TextView rank = holder.rank; - SimpleDraweeView avatar = holder.avatar; - TextView username = holder.username; - TextView count = holder.count; - - rank.setText(String.format("%s %d", - holder.getContext().getResources().getString(R.string.rank_prefix), - leaderboardResponse.getRank())); - - avatar.setImageURI( - Uri.parse(leaderboardResponse.getAvatar())); - username.setText(leaderboardResponse.getUsername()); - count.setText(String.format("%s %d", - holder.getContext().getResources().getString(R.string.count_prefix), - leaderboardResponse.getCategoryCount())); - - // When user tap on avatar shows the toast on how to change avatar - // fixing: https://github.com/commons-app/apps-android-commons/issues/47747 - if (currentlyLoggedInUserName == null) { - // If the current login username has not been fetched yet, then fetch it. - final AccountManager accountManager = AccountManager.get(username.getContext()); - final Account[] allAccounts = accountManager.getAccountsByType( - BuildConfig.ACCOUNT_TYPE); - if (allAccounts.length != 0) { - currentlyLoggedInUserName = allAccounts[0].name; - } - } - if (currentlyLoggedInUserName != null && currentlyLoggedInUserName.equals( - leaderboardResponse.getUsername())) { - - avatar.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Toast.makeText(v.getContext(), - R.string.set_up_avatar_toast_string, - Toast.LENGTH_LONG).show(); - } - }); - } - } - - @Override - public int getItemCount() { - return 1; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt new file mode 100644 index 0000000000..34fd5ab581 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/UserDetailAdapter.kt @@ -0,0 +1,91 @@ +package fr.free.nrw.commons.profile.leaderboard + +import android.accounts.AccountManager +import android.net.Uri +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import android.widget.Toast +import androidx.recyclerview.widget.RecyclerView +import com.facebook.drawee.view.SimpleDraweeView +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.R +import fr.free.nrw.commons.profile.leaderboard.UserDetailAdapter.DataViewHolder +import java.util.Locale + +/** + * This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard + */ +class UserDetailAdapter(private val leaderboardResponse: LeaderboardResponse) : + RecyclerView.Adapter() { + /** + * Stores the username of currently logged in user. + */ + private var currentlyLoggedInUserName: String? = null + + class DataViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val rank: TextView = itemView.findViewById(R.id.rank) + val avatar: SimpleDraweeView = itemView.findViewById(R.id.avatar) + val username: TextView = itemView.findViewById(R.id.username) + val count: TextView = itemView.findViewById(R.id.count) + } + + /** + * Overrides the onCreateViewHolder and sets the view with leaderboard user element layout + * @param parent + * @param viewType + * @return + */ + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): DataViewHolder = DataViewHolder( + LayoutInflater.from(parent.context) + .inflate(R.layout.leaderboard_user_element, parent, false) + ) + + /** + * Overrides the onBindViewHolder Set the view at the specific position with the specific value + * @param holder + * @param position + */ + override fun onBindViewHolder(holder: DataViewHolder, position: Int) = with(holder) { + val resources = itemView.context.resources + + avatar.setImageURI(Uri.parse(leaderboardResponse.avatar)) + username.text = leaderboardResponse.username + rank.text = String.format( + Locale.getDefault(), + "%s %d", + resources.getString(R.string.rank_prefix), + leaderboardResponse.rank + ) + count.text = String.format( + Locale.getDefault(), + "%s %d", + resources.getString(R.string.count_prefix), + leaderboardResponse.categoryCount + ) + + // When user tap on avatar shows the toast on how to change avatar + // fixing: https://github.com/commons-app/apps-android-commons/issues/47747 + if (currentlyLoggedInUserName == null) { + // If the current login username has not been fetched yet, then fetch it. + val accountManager = AccountManager.get(itemView.context) + val allAccounts = accountManager.getAccountsByType(BuildConfig.ACCOUNT_TYPE) + if (allAccounts.isNotEmpty()) { + currentlyLoggedInUserName = allAccounts[0].name + } + } + if (currentlyLoggedInUserName != null && currentlyLoggedInUserName == leaderboardResponse.username) { + avatar.setOnClickListener { v: View -> + Toast.makeText( + v.context, R.string.set_up_avatar_toast_string, Toast.LENGTH_LONG + ).show() + } + } + } + + override fun getItemCount(): Int = 1 +} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java deleted file mode 100644 index fece771100..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -package fr.free.nrw.commons.profile.leaderboard; - -import androidx.annotation.NonNull; -import androidx.lifecycle.ViewModel; -import androidx.lifecycle.ViewModelProvider; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import javax.inject.Inject; - -/** - * This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class - * for leaderboardListViewModel - */ -public class ViewModelFactory implements ViewModelProvider.Factory { - - private OkHttpJsonApiClient okHttpJsonApiClient; - private SessionManager sessionManager; - - - @Inject - public ViewModelFactory(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) { - this.okHttpJsonApiClient = okHttpJsonApiClient; - this.sessionManager = sessionManager; - } - - - /** - * Creats a new LeaderboardListViewModel - * @param modelClass - * @param - * @return - */ - @NonNull - @Override - public T create(@NonNull Class modelClass) { - if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) { - return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager); - } - throw new IllegalArgumentException("Unknown class name"); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt new file mode 100644 index 0000000000..f325355e0f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/leaderboard/ViewModelFactory.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.profile.leaderboard + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import javax.inject.Inject + + +/** + * This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class + * for leaderboardListViewModel + */ +class ViewModelFactory @Inject constructor( + private val okHttpJsonApiClient: OkHttpJsonApiClient, + private val sessionManager: SessionManager +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T = + if (modelClass.isAssignableFrom(LeaderboardListViewModel::class.java)) { + LeaderboardListViewModel(okHttpJsonApiClient, sessionManager) as T + } else { + throw IllegalArgumentException("Unknown class name") + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java deleted file mode 100644 index 51d806a88d..0000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.java +++ /dev/null @@ -1,116 +0,0 @@ -package fr.free.nrw.commons.leaderboard; - -import com.google.gson.Gson; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -/** - * This class tests the Leaderboard API calls - */ -public class LeaderboardApiTest { - - MockWebServer server; - private static final String TEST_USERNAME = "user"; - private static final String TEST_AVATAR = "avatar"; - private static final int TEST_USER_RANK = 1; - private static final int TEST_USER_COUNT = 0; - - private static final String FILE_NAME = "leaderboard_sample_response.json"; - private static final String ENDPOINT = "/leaderboard.py"; - - /** - * This method initialises a Mock Server - */ - @Before - public void initTest() { - server = new MockWebServer(); - } - - /** - * This method will setup a Mock Server and load Test JSON Response File - * @throws Exception - */ - @Before - public void setUp() throws Exception { - - String testResponseBody = convertStreamToString(getClass().getClassLoader().getResourceAsStream(FILE_NAME)); - - server.enqueue(new MockResponse().setBody(testResponseBody)); - server.start(); - } - - /** - * This method converts a Input Stream to String - * @param is takes Input Stream of JSON File as Parameter - * @return a String with JSON data - * @throws Exception - */ - private static String convertStreamToString(InputStream is) throws Exception { - BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } - - /** - * This method will call the Mock Server and Test it with sample values. - * It will test the Leaderboard API call functionality and check if the object is - * being created with the correct values - * @throws IOException - */ - @Test - public void apiTest() throws IOException { - HttpUrl httpUrl = server.url(ENDPOINT); - LeaderboardResponse response = sendRequest(new OkHttpClient(), httpUrl); - - Assert.assertEquals(TEST_AVATAR, response.getAvatar()); - Assert.assertEquals(TEST_USERNAME, response.getUsername()); - Assert.assertEquals(Integer.valueOf(TEST_USER_RANK), response.getRank()); - Assert.assertEquals(Integer.valueOf(TEST_USER_COUNT), response.getCategoryCount()); - } - - /** - * This method will call the Mock API and returns the Leaderboard Response Object - * @param okHttpClient - * @param httpUrl - * @return Leaderboard Response Object - * @throws IOException - */ - private LeaderboardResponse sendRequest(OkHttpClient okHttpClient, HttpUrl httpUrl) - throws IOException { - Request request = new Builder().url(httpUrl).build(); - Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - Gson gson = new Gson(); - return gson.fromJson(response.body().string(), LeaderboardResponse.class); - } - return null; - } - - /** - * This method shuts down the Mock Server - * @throws IOException - */ - @After - public void shutdown() throws IOException { - server.shutdown(); - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt new file mode 100644 index 0000000000..ac0da42f35 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/LeaderboardApiTest.kt @@ -0,0 +1,121 @@ +package fr.free.nrw.commons.leaderboard + +import com.google.gson.Gson +import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + +/** + * This class tests the Leaderboard API calls + */ +class LeaderboardApiTest { + lateinit var server: MockWebServer + + /** + * This method initialises a Mock Server + */ + @Before + fun initTest() { + server = MockWebServer() + } + + /** + * This method will setup a Mock Server and load Test JSON Response File + * @throws Exception + */ + @Before + @Throws(Exception::class) + fun setUp() { + val testResponseBody = convertStreamToString( + javaClass.classLoader!!.getResourceAsStream(FILE_NAME) + ) + + server.enqueue(MockResponse().setBody(testResponseBody)) + server.start() + } + + /** + * This method will call the Mock Server and Test it with sample values. + * It will test the Leaderboard API call functionality and check if the object is + * being created with the correct values + * @throws IOException + */ + @Test + @Throws(IOException::class) + fun apiTest() { + val httpUrl = server.url(ENDPOINT) + val response = sendRequest(OkHttpClient(), httpUrl) + + Assert.assertEquals(TEST_AVATAR, response!!.avatar) + Assert.assertEquals(TEST_USERNAME, response.username) + Assert.assertEquals(TEST_USER_RANK, response.rank) + Assert.assertEquals(TEST_USER_COUNT, response.categoryCount) + } + + /** + * This method will call the Mock API and returns the Leaderboard Response Object + * @param okHttpClient + * @param httpUrl + * @return Leaderboard Response Object + * @throws IOException + */ + @Throws(IOException::class) + private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): LeaderboardResponse? { + val request: Request = Request.Builder().url(httpUrl).build() + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val gson = Gson() + return gson.fromJson(response.body!!.string(), LeaderboardResponse::class.java) + } + return null + } + + /** + * This method shuts down the Mock Server + * @throws IOException + */ + @After + @Throws(IOException::class) + fun shutdown() { + server.shutdown() + } + + companion object { + private const val TEST_USERNAME = "user" + private const val TEST_AVATAR = "avatar" + private const val TEST_USER_RANK = 1 + private const val TEST_USER_COUNT = 0 + + private const val FILE_NAME = "leaderboard_sample_response.json" + private const val ENDPOINT = "/leaderboard.py" + + /** + * This method converts a Input Stream to String + * @param is takes Input Stream of JSON File as Parameter + * @return a String with JSON data + * @throws Exception + */ + @Throws(Exception::class) + private fun convertStreamToString(`is`: InputStream): String { + val reader = BufferedReader(InputStreamReader(`is`)) + val sb = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + sb.append(line).append("\n") + } + reader.close() + return sb.toString() + } + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java deleted file mode 100644 index 7c2b25d3b6..0000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package fr.free.nrw.commons.leaderboard; - -import com.google.gson.Gson; -import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Request.Builder; -import okhttp3.Response; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import org.junit.After; -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -public class UpdateAvatarApiTest { - - private static final String TEST_USERNAME = "user"; - private static final String TEST_STATUS = "200"; - private static final String TEST_MESSAGE = "Avatar Updated"; - private static final String FILE_NAME = "update_leaderboard_avatar_sample_response.json"; - private static final String ENDPOINT = "/update_avatar.py"; - MockWebServer server; - - /** - * This method converts a Input Stream to String - * - * @param is takes Input Stream of JSON File as Parameter - * @return a String with JSON data - * @throws Exception - */ - private static String convertStreamToString(final InputStream is) throws Exception { - final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); - final StringBuilder sb = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - sb.append(line).append("\n"); - } - reader.close(); - return sb.toString(); - } - - /** - * This method initialises a Mock Server - */ - @Before - public void initTest() { - server = new MockWebServer(); - } - - /** - * This method will setup a Mock Server and load Test JSON Response File - * - * @throws Exception - */ - @Before - public void setUp() throws Exception { - - final String testResponseBody = convertStreamToString( - getClass().getClassLoader().getResourceAsStream(FILE_NAME)); - - server.enqueue(new MockResponse().setBody(testResponseBody)); - server.start(); - } - - /** - * This method will call the Mock Server and Test it with sample values. It will test the Update - * Avatar API call functionality and check if the object is being created with the correct - * values - * - * @throws IOException - */ - @Test - public void apiTest() throws IOException { - final HttpUrl httpUrl = server.url(ENDPOINT); - final UpdateAvatarResponse response = sendRequest(new OkHttpClient(), httpUrl); - - Assert.assertEquals(TEST_USERNAME, response.getUser()); - Assert.assertEquals(TEST_STATUS, response.getStatus()); - Assert.assertEquals(TEST_MESSAGE, response.getMessage()); - } - - /** - * This method will call the Mock API and returns the Update Avatar Response Object - * - * @param okHttpClient - * @param httpUrl - * @return Update Avatar Response Object - * @throws IOException - */ - private UpdateAvatarResponse sendRequest(final OkHttpClient okHttpClient, final HttpUrl httpUrl) - throws IOException { - final Request request = new Builder().url(httpUrl).build(); - final Response response = okHttpClient.newCall(request).execute(); - if (response.isSuccessful()) { - final Gson gson = new Gson(); - return gson.fromJson(response.body().string(), UpdateAvatarResponse.class); - } - return null; - } - - /** - * This method shuts down the Mock Server - * - * @throws IOException - */ - @After - public void shutdown() throws IOException { - server.shutdown(); - } -} - diff --git a/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt new file mode 100644 index 0000000000..6b7f064cf4 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/leaderboard/UpdateAvatarApiTest.kt @@ -0,0 +1,127 @@ +package fr.free.nrw.commons.leaderboard + +import com.google.gson.Gson +import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader + +class UpdateAvatarApiTest { + lateinit var server: MockWebServer + + /** + * This method initialises a Mock Server + */ + @Before + fun initTest() { + server = MockWebServer() + } + + /** + * This method will setup a Mock Server and load Test JSON Response File + * + * @throws Exception + */ + @Before + @Throws(Exception::class) + fun setUp() { + val testResponseBody = convertStreamToString( + javaClass.classLoader!!.getResourceAsStream(FILE_NAME) + ) + + server.enqueue(MockResponse().setBody(testResponseBody)) + server.start() + } + + /** + * This method will call the Mock Server and Test it with sample values. It will test the Update + * Avatar API call functionality and check if the object is being created with the correct + * values + * + * @throws IOException + */ + @Test + @Throws(IOException::class) + fun apiTest() { + val httpUrl = server.url(ENDPOINT) + val response = sendRequest(OkHttpClient(), httpUrl) + Assert.assertNotNull(response) + + with(response!!) { + Assert.assertEquals(TEST_USERNAME, user) + Assert.assertEquals(TEST_STATUS, status) + Assert.assertEquals(TEST_MESSAGE, message) + } + } + + /** + * This method will call the Mock API and returns the Update Avatar Response Object + * + * @param okHttpClient + * @param httpUrl + * @return Update Avatar Response Object + * @throws IOException + */ + @Throws(IOException::class) + private fun sendRequest(okHttpClient: OkHttpClient, httpUrl: HttpUrl): UpdateAvatarResponse? { + val request: Request = Request.Builder().url(httpUrl).build() + val response = okHttpClient.newCall(request).execute() + if (response.isSuccessful) { + val gson = Gson() + return gson.fromJson( + response.body!!.string(), + UpdateAvatarResponse::class.java + ) + } + return null + } + + /** + * This method shuts down the Mock Server + * + * @throws IOException + */ + @After + @Throws(IOException::class) + fun shutdown() { + server.shutdown() + } + + companion object { + private const val TEST_USERNAME = "user" + private const val TEST_STATUS = "200" + private const val TEST_MESSAGE = "Avatar Updated" + private const val FILE_NAME = "update_leaderboard_avatar_sample_response.json" + private const val ENDPOINT = "/update_avatar.py" + + /** + * This method converts a Input Stream to String + * + * @param is takes Input Stream of JSON File as Parameter + * @return a String with JSON data + * @throws Exception + */ + @Throws(Exception::class) + private fun convertStreamToString(`is`: InputStream): String { + val reader = BufferedReader(InputStreamReader(`is`)) + val sb = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + sb.append(line).append("\n") + } + reader.close() + return sb.toString() + } + } +} + From 9dd504e56096bc5892edff901d3a939e50846939 Mon Sep 17 00:00:00 2001 From: "translatewiki.net" Date: Thu, 5 Dec 2024 13:01:47 +0100 Subject: [PATCH 048/231] Localisation updates from https://translatewiki.net. --- app/src/main/res/values-tr/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 54593c681f..2e4e464813 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -21,6 +21,7 @@ * Okkerem * Oyuncu * Rapsar +* RuzDD * SaldırganSincap * Sayginer * Sezgin İbiş @@ -146,6 +147,7 @@ Kategori ara Medyanızın tasvir ettiği ögeleri arayın (dağ, Tac Mahal, vb.) Kaydet + Taşma menüsü Yenile Liste !Henüz yükleme yok) @@ -800,6 +802,7 @@ Lütfen bir yorum girin Tartışma \' %1$s \' öğesi hakkında bir şeyler yazın. Herkes tarafından görülebilir olacaktır. + \'%1$s\' artık yok, dolayısı ile resmi çekilemez. Diğer sorun veya bilgi (lütfen aşağıda açıklayınız). Geri bildiriminiz aşağıdaki wiki sayfasına gönderilir: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Tüm yüklemeleri iptal etmek istediğinizden emin misiniz? @@ -807,5 +810,10 @@ Yüklemeler Beklemede Başarısız + Sil + İptal + %1$s klasörü başarıyla silindi + %1$s klasörü silinemedi Bu yerin zaten bir resmi var. + Şimdi bu yerin bir resime sahip olup olmadığı denetleniyor. From 3777f18bf9c7efa4c679023b91ba9bad6cc10ad0 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Thu, 5 Dec 2024 08:13:38 -0600 Subject: [PATCH 049/231] Convert mwapi/wikidata to kotlin (part 1) (#5991) * Convert OkHttpJsonApiClient and CategoryApi to kotlin * Convert GsonUtil to kotlin * Convert WikidataConstants to kotlin * Convert WikidataEditListener to kotlin * Convert WikidataEditService to kotlin * work in progress * Convert RequiredFieldsCheckOnReadTypeAdapterFactory to kotlin * Converted type adapters * Convert WikiSiteTypeAdapter to kotlin * Fixed nullability --- .../commons/campaigns/CampaignsPresenter.kt | 4 +- .../free/nrw/commons/di/NetworkingModule.kt | 3 +- .../free/nrw/commons/mwapi/CategoryApi.java | 99 --- .../fr/free/nrw/commons/mwapi/CategoryApi.kt | 83 +++ .../commons/mwapi/OkHttpJsonApiClient.java | 677 ------------------ .../nrw/commons/mwapi/OkHttpJsonApiClient.kt | 543 ++++++++++++++ .../free/nrw/commons/upload/FileProcessor.kt | 4 +- .../nrw/commons/upload/worker/UploadWorker.kt | 4 +- .../commons/wikidata/CommonsServiceFactory.kt | 5 +- .../free/nrw/commons/wikidata/GsonUtil.java | 34 - .../fr/free/nrw/commons/wikidata/GsonUtil.kt | 29 + .../commons/wikidata/WikidataConstants.java | 11 - .../nrw/commons/wikidata/WikidataConstants.kt | 11 + .../wikidata/WikidataEditListener.java | 16 - .../commons/wikidata/WikidataEditListener.kt | 11 + .../wikidata/WikidataEditListenerImpl.java | 20 - .../wikidata/WikidataEditListenerImpl.kt | 13 + .../commons/wikidata/WikidataEditService.java | 271 ------- .../commons/wikidata/WikidataEditService.kt | 252 +++++++ .../wikidata/json/NamespaceTypeAdapter.java | 29 - .../wikidata/json/NamespaceTypeAdapter.kt | 26 + .../json/PostProcessingTypeAdapter.java | 34 - .../json/PostProcessingTypeAdapter.kt | 35 + ...edFieldsCheckOnReadTypeAdapterFactory.java | 94 --- ...iredFieldsCheckOnReadTypeAdapterFactory.kt | 75 ++ .../json/RuntimeTypeAdapterFactory.java | 280 -------- .../json/RuntimeTypeAdapterFactory.kt | 273 +++++++ .../commons/wikidata/json/UriTypeAdapter.java | 22 - .../commons/wikidata/json/UriTypeAdapter.kt | 19 + .../wikidata/json/WikiSiteTypeAdapter.java | 63 -- .../wikidata/json/WikiSiteTypeAdapter.kt | 61 ++ .../wikidata/json/annotations/Required.java | 21 - .../wikidata/json/annotations/Required.kt | 12 + .../model/notifications/Notification.java | 2 +- .../nrw/commons/wikidata/mwapi/UserInfo.java | 34 - .../nrw/commons/wikidata/mwapi/UserInfo.kt | 21 + .../free/nrw/commons/MockWebServerTest.java | 2 +- .../campaigns/CampaignsPresenterTest.kt | 6 +- .../free/nrw/commons/mwapi/UserClientTest.kt | 9 +- .../nearby/NearbyParentFragmentUnitTest.kt | 2 +- .../notification/NotificationClientTest.kt | 28 +- 41 files changed, 1491 insertions(+), 1747 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java create mode 100644 app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt diff --git a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt index ffbf925406..4743e0e543 100644 --- a/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt +++ b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignsPresenter.kt @@ -52,12 +52,12 @@ class CampaignsPresenter @Inject constructor( return } - okHttpJsonApiClient.campaigns + okHttpJsonApiClient.getCampaigns() .observeOn(mainThreadScheduler) .subscribeOn(ioScheduler) .doOnSubscribe { disposable = it } .subscribe({ campaignResponseDTO -> - val campaigns = campaignResponseDTO.campaigns?.toMutableList() + val campaigns = campaignResponseDTO?.campaigns?.toMutableList() if (campaigns.isNullOrEmpty()) { Timber.e("The campaigns list is empty") view!!.showCampaigns(null) diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt index 5ecc041209..7ca3b4fd03 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -170,14 +170,13 @@ class NetworkingModule { @Named(NAMED_WIKI_DATA_WIKI_SITE) fun provideWikidataWikiSite(): WikiSite = WikiSite(BuildConfig.WIKIDATA_URL) - /** * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. * @return returns a singleton Gson instance */ @Provides @Singleton - fun provideGson(): Gson = GsonUtil.getDefaultGson() + fun provideGson(): Gson = GsonUtil.defaultGson @Provides @Singleton diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java deleted file mode 100644 index f587893c5f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java +++ /dev/null @@ -1,99 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import static fr.free.nrw.commons.category.CategoryClientKt.CATEGORY_PREFIX; - -import com.google.gson.Gson; -import fr.free.nrw.commons.BuildConfig; -import fr.free.nrw.commons.category.CategoryItem; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryPage; -import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse; -import io.reactivex.Single; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; -import javax.inject.Inject; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import timber.log.Timber; - -/** - * Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates - * with nearby Commons categories. Parses the results using GSON to obtain a list of relevant - * categories. Note: that caller is responsible for executing the request() method on a background - * thread. - */ -public class CategoryApi { - - private final OkHttpClient okHttpClient; - private final Gson gson; - - @Inject - public CategoryApi(final OkHttpClient okHttpClient, final Gson gson) { - this.okHttpClient = okHttpClient; - this.gson = gson; - } - - public Single> request(String coords) { - return Single.fromCallable(() -> { - HttpUrl apiUrl = buildUrl(coords); - Timber.d("URL: %s", apiUrl.toString()); - - Request request = new Request.Builder().get().url(apiUrl).build(); - Response response = okHttpClient.newCall(request).execute(); - ResponseBody body = response.body(); - if (body == null) { - return Collections.emptyList(); - } - - MwQueryResponse apiResponse = gson.fromJson(body.charStream(), MwQueryResponse.class); - Set categories = new LinkedHashSet<>(); - if (apiResponse != null && apiResponse.query() != null && apiResponse.query().pages() != null) { - for (MwQueryPage page : apiResponse.query().pages()) { - if (page.categories() != null) { - for (MwQueryPage.Category category : page.categories()) { - categories.add(new CategoryItem(category.title().replace(CATEGORY_PREFIX, ""), "", "", false)); - } - } - } - } - return new ArrayList<>(categories); - }); - } - - /** - * Builds URL with image coords for MediaWiki API calls - * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 - * - * @param coords Coordinates to build query with - * @return URL for API query - */ - private HttpUrl buildUrl(final String coords) { - return HttpUrl - .parse(BuildConfig.WIKIMEDIA_API_HOST) - .newBuilder() - .addQueryParameter("action", "query") - .addQueryParameter("prop", "categories|coordinates|pageprops") - .addQueryParameter("format", "json") - .addQueryParameter("clshow", "!hidden") - .addQueryParameter("coprop", "type|name|dim|country|region|globe") - .addQueryParameter("codistancefrompoint", coords) - .addQueryParameter("generator", "geosearch") - .addQueryParameter("ggscoord", coords) - .addQueryParameter("ggsradius", "10000") - .addQueryParameter("ggslimit", "10") - .addQueryParameter("ggsnamespace", "6") - .addQueryParameter("ggsprop", "type|name|dim|country|region|globe") - .addQueryParameter("ggsprimary", "all") - .addQueryParameter("formatversion", "2") - .build(); - } - -} - - - diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt new file mode 100644 index 0000000000..1f8c51187b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.kt @@ -0,0 +1,83 @@ +package fr.free.nrw.commons.mwapi + +import com.google.gson.Gson +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.category.CATEGORY_PREFIX +import fr.free.nrw.commons.category.CategoryItem +import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse +import io.reactivex.Single +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import timber.log.Timber +import javax.inject.Inject + +/** + * Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates + * with nearby Commons categories. Parses the results using GSON to obtain a list of relevant + * categories. Note: that caller is responsible for executing the request() method on a background + * thread. + */ +class CategoryApi @Inject constructor( + private val okHttpClient: OkHttpClient, + private val gson: Gson +) { + private val apiUrl : HttpUrl by lazy { BuildConfig.WIKIMEDIA_API_HOST.toHttpUrlOrNull()!! } + + fun request(coords: String): Single> = Single.fromCallable { + val apiUrl = buildUrl(coords) + Timber.d("URL: %s", apiUrl.toString()) + + val request: Request = Request.Builder().get().url(apiUrl).build() + val response = okHttpClient.newCall(request).execute() + val body = response.body ?: return@fromCallable emptyList() + + val apiResponse = gson.fromJson(body.charStream(), MwQueryResponse::class.java) + val categories: MutableSet = mutableSetOf() + if (apiResponse?.query() != null && apiResponse.query()!!.pages() != null) { + for (page in apiResponse.query()!!.pages()!!) { + if (page.categories() != null) { + for (category in page.categories()!!) { + categories.add( + CategoryItem( + name = category.title().replace(CATEGORY_PREFIX, ""), + description = "", + thumbnail = "", + isSelected = false + ) + ) + } + } + } + } + ArrayList(categories) + } + + /** + * Builds URL with image coords for MediaWiki API calls + * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 + * + * @param coords Coordinates to build query with + * @return URL for API query + */ + private fun buildUrl(coords: String): HttpUrl = apiUrl.newBuilder() + .addQueryParameter("action", "query") + .addQueryParameter("prop", "categories|coordinates|pageprops") + .addQueryParameter("format", "json") + .addQueryParameter("clshow", "!hidden") + .addQueryParameter("coprop", "type|name|dim|country|region|globe") + .addQueryParameter("codistancefrompoint", coords) + .addQueryParameter("generator", "geosearch") + .addQueryParameter("ggscoord", coords) + .addQueryParameter("ggsradius", "10000") + .addQueryParameter("ggslimit", "10") + .addQueryParameter("ggsnamespace", "6") + .addQueryParameter("ggsprop", "type|name|dim|country|region|globe") + .addQueryParameter("ggsprimary", "all") + .addQueryParameter("formatversion", "2") + .build() +} + + + diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java deleted file mode 100644 index 8ed37a2937..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ /dev/null @@ -1,677 +0,0 @@ -package fr.free.nrw.commons.mwapi; - -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LEADERBOARD_END_POINT; -import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.UPDATE_AVATAR_END_POINT; - -import android.text.TextUtils; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.gson.Gson; -import fr.free.nrw.commons.campaigns.CampaignResponseDTO; -import fr.free.nrw.commons.explore.depictions.DepictsClient; -import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.nearby.Place; -import fr.free.nrw.commons.nearby.model.ItemsClass; -import fr.free.nrw.commons.nearby.model.NearbyResponse; -import fr.free.nrw.commons.nearby.model.NearbyResultItem; -import fr.free.nrw.commons.nearby.model.PlaceBindings; -import fr.free.nrw.commons.profile.achievements.FeaturedImages; -import fr.free.nrw.commons.profile.achievements.FeedbackResponse; -import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse; -import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse; -import fr.free.nrw.commons.upload.FileUtils; -import fr.free.nrw.commons.upload.structure.depictions.DepictedItem; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse; -import io.reactivex.Observable; -import io.reactivex.Single; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.inject.Inject; -import javax.inject.Singleton; -import okhttp3.HttpUrl; -import okhttp3.OkHttpClient; -import okhttp3.Request; -import okhttp3.Response; -import okhttp3.ResponseBody; -import org.jetbrains.annotations.NotNull; -import timber.log.Timber; - -/** - * Test methods in ok http api client - */ -@Singleton -public class OkHttpJsonApiClient { - - private final OkHttpClient okHttpClient; - private final DepictsClient depictsClient; - private final HttpUrl wikiMediaToolforgeUrl; - private final String sparqlQueryUrl; - private final String campaignsUrl; - private final Gson gson; - - - @Inject - public OkHttpJsonApiClient(OkHttpClient okHttpClient, - DepictsClient depictsClient, - HttpUrl wikiMediaToolforgeUrl, - String sparqlQueryUrl, - String campaignsUrl, - Gson gson) { - this.okHttpClient = okHttpClient; - this.depictsClient = depictsClient; - this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl; - this.sparqlQueryUrl = sparqlQueryUrl; - this.campaignsUrl = campaignsUrl; - this.gson = gson; - } - - /** - * The method will gradually calls the leaderboard API and fetches the leaderboard - * - * @param userName username of leaderboard user - * @param duration duration for leaderboard - * @param category category for leaderboard - * @param limit page size limit for list - * @param offset offset for the list - * @return LeaderboardResponse object - */ - @NonNull - public Observable getLeaderboard(String userName, String duration, - String category, String limit, String offset) { - final String fetchLeaderboardUrlTemplate = wikiMediaToolforgeUrl - + LEADERBOARD_END_POINT; - String url = String.format(Locale.ENGLISH, - fetchLeaderboardUrlTemplate, - userName, - duration, - category, - limit, - offset); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", userName); - urlBuilder.addQueryParameter("duration", duration); - urlBuilder.addQueryParameter("category", category); - urlBuilder.addQueryParameter("limit", limit); - urlBuilder.addQueryParameter("offset", offset); - Timber.i("Url %s", urlBuilder.toString()); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - return Observable.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return new LeaderboardResponse(); - } - Timber.d("Response for leaderboard is %s", json); - try { - return gson.fromJson(json, LeaderboardResponse.class); - } catch (Exception e) { - return new LeaderboardResponse(); - } - } - return new LeaderboardResponse(); - }); - } - - /** - * This method will update the leaderboard user avatar - * - * @param username username to update - * @param avatar url of the new avatar - * @return UpdateAvatarResponse object - */ - @NonNull - public Single setAvatar(String username, String avatar) { - final String urlTemplate = wikiMediaToolforgeUrl - + UPDATE_AVATAR_END_POINT; - return Single.fromCallable(() -> { - String url = String.format(Locale.ENGLISH, - urlTemplate, - username, - avatar); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", username); - urlBuilder.addQueryParameter("avatar", avatar); - Timber.i("Url %s", urlBuilder.toString()); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - try { - return gson.fromJson(json, UpdateAvatarResponse.class); - } catch (Exception e) { - return new UpdateAvatarResponse(); - } - } - return null; - }); - } - - @NonNull - public Single getUploadCount(String userName) { - HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); - urlBuilder - .addPathSegments("uploadsbyuser.py") - .addQueryParameter("user", userName); - - if (ConfigUtils.isBetaFlavour()) { - urlBuilder.addQueryParameter("labs", "commonswiki"); - } - - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.isSuccessful()) { - ResponseBody responseBody = response.body(); - if (null != responseBody) { - String responseBodyString = responseBody.string().trim(); - if (!TextUtils.isEmpty(responseBodyString)) { - try { - return Integer.parseInt(responseBodyString); - } catch (NumberFormatException e) { - Timber.e(e); - } - } - } - } - return 0; - }); - } - - @NonNull - public Single getWikidataEdits(String userName) { - HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder(); - urlBuilder - .addPathSegments("wikidataedits.py") - .addQueryParameter("user", userName); - - if (ConfigUtils.isBetaFlavour()) { - urlBuilder.addQueryParameter("labs", "commonswiki"); - } - - Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - return Single.fromCallable(() -> { - Response response = okHttpClient.newCall(request).execute(); - if (response != null && - response.isSuccessful() && response.body() != null) { - String json = response.body().string(); - if (json == null) { - return 0; - } - // Extract JSON from response - json = json.substring(json.indexOf('{')); - GetWikidataEditCountResponse countResponse = gson - .fromJson(json, GetWikidataEditCountResponse.class); - if (null != countResponse) { - return countResponse.getWikidataEditCount(); - } - } - return 0; - }); - } - - /** - * This takes userName as input, which is then used to fetch the feedback/achievements - * statistics using OkHttp and JavaRx. This function return JSONObject - * - * @param userName MediaWiki user name - * @return - */ - public Single getAchievements(String userName) { - final String fetchAchievementUrlTemplate = - wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki" - : "/feedback.py"); - return Single.fromCallable(() -> { - String url = String.format( - Locale.ENGLISH, - fetchAchievementUrlTemplate, - userName); - HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); - urlBuilder.addQueryParameter("user", userName); - Request request = new Request.Builder() - .url(urlBuilder.toString()) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - // Extract JSON from response - json = json.substring(json.indexOf('{')); - Timber.d("Response for achievements is %s", json); - try { - return gson.fromJson(json, FeedbackResponse.class); - } catch (Exception e) { - return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, ""); - } - - - } - return null; - }); - } - - /** - * Make API Call to get Nearby Places - * - * @param cur Search lat long - * @param language Language - * @param radius Search Radius - * @return - * @throws Exception - */ - @Nullable - public List getNearbyPlaces(final LatLng cur, final String language, final double radius, - final String customQuery) - throws Exception { - - Timber.d("Fetching nearby items at radius %s", radius); - Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); - final String wikidataQuery; - if (customQuery != null) { - wikidataQuery = customQuery; - } else { - wikidataQuery = FileUtils.readFromResource( - "/queries/radius_query_for_upload_wizard.rq"); - } - final String query = wikidataQuery - .replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius)) - .replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude())) - .replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude())) - .replace("${LANG}", language); - - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - final Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - final String json = response.body().string(); - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place placeFromNearbyItem = Place.from(item); - placeFromNearbyItem.setMonument(false); - places.add(placeFromNearbyItem); - } - return places; - } - throw new Exception(response.message()); - } - - /** - * Retrieves nearby places based on screen coordinates and optional query parameters. - * - * @param screenTopRight The top right corner of the screen (latitude, longitude). - * @param screenBottomLeft The bottom left corner of the screen (latitude, longitude). - * @param language The language for the query. - * @param shouldQueryForMonuments Flag indicating whether to include monuments in the query. - * @param customQuery Optional custom SPARQL query to use instead of default - * queries. - * @return A list of nearby places. - * @throws Exception If an error occurs during the retrieval process. - */ - @Nullable - public List getNearbyPlaces( - final fr.free.nrw.commons.location.LatLng screenTopRight, - final fr.free.nrw.commons.location.LatLng screenBottomLeft, final String language, - final boolean shouldQueryForMonuments, final String customQuery) - throws Exception { - - Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); - - final String wikidataQuery; - if (customQuery != null) { - wikidataQuery = customQuery; - } else if (!shouldQueryForMonuments) { - wikidataQuery = FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq"); - } else { - wikidataQuery = FileUtils.readFromResource( - "/queries/rectangle_query_for_nearby_monuments.rq"); - } - - final double westCornerLat = screenTopRight.getLatitude(); - final double westCornerLong = screenTopRight.getLongitude(); - final double eastCornerLat = screenBottomLeft.getLatitude(); - final double eastCornerLong = screenBottomLeft.getLongitude(); - - final String query = wikidataQuery - .replace("${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat)) - .replace("${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong)) - .replace("${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat)) - .replace("${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong)) - .replace("${LANG}", language); - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - final Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - final String json = response.body().string(); - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place placeFromNearbyItem = Place.from(item); - if (shouldQueryForMonuments && item.getMonument() != null) { - placeFromNearbyItem.setMonument(true); - } else { - placeFromNearbyItem.setMonument(false); - } - places.add(placeFromNearbyItem); - } - return places; - } - throw new Exception(response.message()); - } - - /** - * Retrieves a list of places based on the provided list of places and language. - * - * @param placeList A list of Place objects for which to fetch information. - * @param language The language code to use for the query. - * @return A list of Place objects with additional information retrieved from Wikidata, or null - * if an error occurs. - * @throws IOException If there is an issue with reading the resource file or executing the HTTP - * request. - */ - @Nullable - public List getPlaces( - final List placeList, final String language) throws IOException { - final String wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq"); - String qids = ""; - for (final Place place : placeList) { - qids += "\n" + ("wd:" + place.getWikiDataEntityId()); - } - final String query = wikidataQuery - .replace("${ENTITY}", qids) - .replace("${LANG}", language); - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - try (Response response = okHttpClient.newCall(request).execute()) { - if (response.isSuccessful()) { - final String json = response.body().string(); - final NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class); - final List bindings = nearbyResponse.getResults().getBindings(); - final List places = new ArrayList<>(); - for (final NearbyResultItem item : bindings) { - final Place placeFromNearbyItem = Place.from(item); - places.add(placeFromNearbyItem); - } - return places; - } else { - throw new IOException("Unexpected response code: " + response.code()); - } - } - } - - /** - * Make API Call to get Places - * - * @param leftLatLng Left lat long - * @param rightLatLng Right lat long - * @return - * @throws Exception - */ - @Nullable - public String getPlacesAsKML(final LatLng leftLatLng, final LatLng rightLatLng) - throws Exception { - String kmlString = "\n" + - "\n" + - "\n" + - " "; - List placeBindings = runQuery(leftLatLng, - rightLatLng); - if (placeBindings != null) { - for (PlaceBindings item : placeBindings) { - if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) { - String input = item.getLocation().getValue(); - Pattern pattern = Pattern.compile( - "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"); - Matcher matcher = pattern.matcher(input); - - if (matcher.find()) { - String longStr = matcher.group(1); - String latStr = matcher.group(2); - String itemUrl = item.getItem().getValue(); - String itemName = item.getLabel().getValue().replace("&", "&"); - String itemLatitude = latStr; - String itemLongitude = longStr; - String itemClass = item.getClas().getValue(); - - String formattedItemName = - !itemClass.isEmpty() ? itemName + " (" + itemClass + ")" - : itemName; - - String kmlEntry = "\n \n" + - " " + formattedItemName + "\n" + - " " + itemUrl + "\n" + - " \n" + - " " + itemLongitude + "," - + itemLatitude - + "\n" + - " \n" + - " "; - kmlString = kmlString + kmlEntry; - } else { - Timber.e("No match found"); - } - } - } - } - kmlString = kmlString + "\n \n" + - "\n"; - return kmlString; - } - - /** - * Make API Call to get Places - * - * @param leftLatLng Left lat long - * @param rightLatLng Right lat long - * @return - * @throws Exception - */ - @Nullable - public String getPlacesAsGPX(final LatLng leftLatLng, final LatLng rightLatLng) - throws Exception { - String gpxString = "\n" + - "" - + "\n"; - - List placeBindings = runQuery(leftLatLng, rightLatLng); - if (placeBindings != null) { - for (PlaceBindings item : placeBindings) { - if (item.getItem() != null && item.getLabel() != null && item.getClas() != null) { - String input = item.getLocation().getValue(); - Pattern pattern = Pattern.compile( - "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)"); - Matcher matcher = pattern.matcher(input); - - if (matcher.find()) { - String longStr = matcher.group(1); - String latStr = matcher.group(2); - String itemUrl = item.getItem().getValue(); - String itemName = item.getLabel().getValue().replace("&", "&"); - String itemLatitude = latStr; - String itemLongitude = longStr; - String itemClass = item.getClas().getValue(); - - String formattedItemName = - !itemClass.isEmpty() ? itemName + " (" + itemClass + ")" - : itemName; - - String gpxEntry = - "\n \n" + - " " + itemName + "\n" + - " " + itemUrl + "\n" + - " "; - gpxString = gpxString + gpxEntry; - - } else { - Timber.e("No match found"); - } - } - } - - } - gpxString = gpxString + "\n"; - return gpxString; - } - - private List runQuery(final LatLng currentLatLng, final LatLng nextLatLng) - throws IOException { - - final String wikidataQuery = FileUtils.readFromResource("/queries/places_query.rq"); - final String query = wikidataQuery - .replace("${LONGITUDE}", - String.format(Locale.ROOT, "%.2f", currentLatLng.getLongitude())) - .replace("${LATITUDE}", String.format(Locale.ROOT, "%.4f", currentLatLng.getLatitude())) - .replace("${NEXT_LONGITUDE}", - String.format(Locale.ROOT, "%.4f", nextLatLng.getLongitude())) - .replace("${NEXT_LATITUDE}", - String.format(Locale.ROOT, "%.4f", nextLatLng.getLatitude())); - - final HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - - final Request request = new Request.Builder() - .url(urlBuilder.build()) - .build(); - - final Response response = okHttpClient.newCall(request).execute(); - if (response.body() != null && response.isSuccessful()) { - final String json = response.body().string(); - final ItemsClass item = gson.fromJson(json, ItemsClass.class); - return item.getResults().getBindings(); - } else { - return null; - } - } - - /** - * Make API Call to get Nearby Places Implementation does not expects a custom query - * - * @param cur Search lat long - * @param language Language - * @param radius Search Radius - * @return - * @throws Exception - */ - @Nullable - public List getNearbyPlaces(final LatLng cur, final String language, final double radius) - throws Exception { - return getNearbyPlaces(cur, language, radius, null); - } - - /** - * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: - * bridge -> suspended bridge, aqueduct, etc - */ - public Single> getChildDepictions(String qid, int startPosition, - int limit) throws IOException { - return depictedItemsFrom( - sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq")); - } - - /** - * Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example: - * bridge -> suspended bridge, aqueduct, etc - */ - public Single> getParentDepictions(String qid, int startPosition, - int limit) throws IOException { - return depictedItemsFrom(sparqlQuery(qid, startPosition, limit, - "/queries/parentclasses_query.rq")); - } - - private Single> depictedItemsFrom(Request request) { - return depictsClient.toDepictions(Single.fromCallable(() -> { - try (ResponseBody body = okHttpClient.newCall(request).execute().body()) { - return gson.fromJson(body.string(), SparqlResponse.class); - } - }).doOnError(Timber::e)); - } - - @NotNull - private Request sparqlQuery(String qid, int startPosition, int limit, String fileName) - throws IOException { - String query = FileUtils.readFromResource(fileName) - .replace("${QID}", qid) - .replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"") - .replace("${LIMIT}", "" + limit) - .replace("${OFFSET}", "" + startPosition); - HttpUrl.Builder urlBuilder = HttpUrl - .parse(sparqlQueryUrl) - .newBuilder() - .addQueryParameter("query", query) - .addQueryParameter("format", "json"); - return new Request.Builder() - .url(urlBuilder.build()) - .build(); - } - - public Single getCampaigns() { - return Single.fromCallable(() -> { - Request request = new Request.Builder().url(campaignsUrl) - .build(); - Response response = okHttpClient.newCall(request).execute(); - if (response != null && response.body() != null && response.isSuccessful()) { - String json = response.body().string(); - if (json == null) { - return null; - } - return gson.fromJson(json, CampaignResponseDTO.class); - } - return null; - }); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt new file mode 100644 index 0000000000..c3ae11b949 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.kt @@ -0,0 +1,543 @@ +package fr.free.nrw.commons.mwapi + +import android.text.TextUtils +import com.google.gson.Gson +import fr.free.nrw.commons.campaigns.CampaignResponseDTO +import fr.free.nrw.commons.explore.depictions.DepictsClient +import fr.free.nrw.commons.location.LatLng +import fr.free.nrw.commons.nearby.Place +import fr.free.nrw.commons.nearby.model.ItemsClass +import fr.free.nrw.commons.nearby.model.NearbyResponse +import fr.free.nrw.commons.nearby.model.PlaceBindings +import fr.free.nrw.commons.profile.achievements.FeaturedImages +import fr.free.nrw.commons.profile.achievements.FeedbackResponse +import fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants +import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse +import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse +import fr.free.nrw.commons.upload.FileUtils +import fr.free.nrw.commons.upload.structure.depictions.DepictedItem +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse +import io.reactivex.Observable +import io.reactivex.Single +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrlOrNull +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import timber.log.Timber +import java.io.IOException +import java.util.Locale +import java.util.regex.Pattern +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Test methods in ok http api client + */ +@Singleton +class OkHttpJsonApiClient @Inject constructor( + private val okHttpClient: OkHttpClient, + private val depictsClient: DepictsClient, + private val wikiMediaToolforgeUrl: HttpUrl, + private val sparqlQueryUrl: String, + private val campaignsUrl: String, + private val gson: Gson +) { + fun getLeaderboard( + userName: String?, duration: String?, + category: String?, limit: String?, offset: String? + ): Observable { + val fetchLeaderboardUrlTemplate = + wikiMediaToolforgeUrl.toString() + LeaderboardConstants.LEADERBOARD_END_POINT + val url = String.format(Locale.ENGLISH, + fetchLeaderboardUrlTemplate, userName, duration, category, limit, offset) + val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("user", userName) + .addQueryParameter("duration", duration) + .addQueryParameter("category", category) + .addQueryParameter("limit", limit) + .addQueryParameter("offset", offset) + Timber.i("Url %s", urlBuilder.toString()) + val request: Request = Request.Builder() + .url(urlBuilder.toString()) + .build() + return Observable.fromCallable({ + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json: String = response.body!!.string() + Timber.d("Response for leaderboard is %s", json) + try { + return@fromCallable gson.fromJson( + json, + LeaderboardResponse::class.java + ) + } catch (e: Exception) { + return@fromCallable LeaderboardResponse() + } + } + LeaderboardResponse() + }) + } + + fun setAvatar(username: String?, avatar: String?): Single { + val urlTemplate = wikiMediaToolforgeUrl + .toString() + LeaderboardConstants.UPDATE_AVATAR_END_POINT + return Single.fromCallable({ + val url = String.format(Locale.ENGLISH, urlTemplate, username, avatar) + val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("user", username) + .addQueryParameter("avatar", avatar) + Timber.i("Url %s", urlBuilder.toString()) + val request: Request = Request.Builder() + .url(urlBuilder.toString()) + .build() + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json: String = response.body!!.string() ?: return@fromCallable null + try { + return@fromCallable gson.fromJson( + json, + UpdateAvatarResponse::class.java + ) + } catch (e: Exception) { + return@fromCallable UpdateAvatarResponse() + } + } + null + }) + } + + fun getUploadCount(userName: String?): Single { + val urlBuilder: HttpUrl.Builder = wikiMediaToolforgeUrl.newBuilder() + .addPathSegments("uploadsbyuser.py") + .addQueryParameter("user", userName) + + if (isBetaFlavour) { + urlBuilder.addQueryParameter("labs", "commonswiki") + } + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + return Single.fromCallable({ + val response: Response = okHttpClient.newCall(request).execute() + if (response != null && response.isSuccessful) { + val responseBody = response.body + if (null != responseBody) { + val responseBodyString = responseBody.string().trim { it <= ' ' } + if (!TextUtils.isEmpty(responseBodyString)) { + try { + return@fromCallable responseBodyString.toInt() + } catch (e: NumberFormatException) { + Timber.e(e) + } + } + } + } + 0 + }) + } + + fun getWikidataEdits(userName: String?): Single { + val urlBuilder: HttpUrl.Builder = wikiMediaToolforgeUrl.newBuilder() + .addPathSegments("wikidataedits.py") + .addQueryParameter("user", userName) + + if (isBetaFlavour) { + urlBuilder.addQueryParameter("labs", "commonswiki") + } + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + return Single.fromCallable({ + val response: Response = okHttpClient.newCall(request).execute() + if (response != null && response.isSuccessful && response.body != null) { + var json: String = response.body!!.string() + // Extract JSON from response + json = json.substring(json.indexOf('{')) + val countResponse = gson + .fromJson( + json, + GetWikidataEditCountResponse::class.java + ) + if (null != countResponse) { + return@fromCallable countResponse.wikidataEditCount + } + } + 0 + }) + } + + fun getAchievements(userName: String?): Single { + val suffix = if (isBetaFlavour) "/feedback.py?labs=commonswiki" else "/feedback.py" + val fetchAchievementUrlTemplate = wikiMediaToolforgeUrl.toString() + suffix + return Single.fromCallable({ + val url = String.format( + Locale.ENGLISH, + fetchAchievementUrlTemplate, + userName + ) + val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder() + .addQueryParameter("user", userName) + val request: Request = Request.Builder() + .url(urlBuilder.toString()) + .build() + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + var json: String = response.body!!.string() + // Extract JSON from response + json = json.substring(json.indexOf('{')) + Timber.d("Response for achievements is %s", json) + try { + return@fromCallable gson.fromJson( + json, + FeedbackResponse::class.java + ) + } catch (e: Exception) { + return@fromCallable FeedbackResponse(0, 0, 0, FeaturedImages(0, 0), 0, "") + } + } + null + }) + } + + @JvmOverloads + @Throws(Exception::class) + fun getNearbyPlaces( + cur: LatLng, language: String, radius: Double, + customQuery: String? = null + ): List? { + Timber.d("Fetching nearby items at radius %s", radius) + Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString()) + val wikidataQuery: String = if (customQuery != null) { + customQuery + } else { + FileUtils.readFromResource("/queries/radius_query_for_upload_wizard.rq") + } + val query = wikidataQuery + .replace("\${RAD}", String.format(Locale.ROOT, "%.2f", radius)) + .replace("\${LAT}", String.format(Locale.ROOT, "%.4f", cur.latitude)) + .replace("\${LONG}", String.format(Locale.ROOT, "%.4f", cur.longitude)) + .replace("\${LANG}", language) + + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + val response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json = response.body!!.string() + val nearbyResponse = gson.fromJson(json, NearbyResponse::class.java) + val bindings = nearbyResponse.results.bindings + val places: MutableList = ArrayList() + for (item in bindings) { + val placeFromNearbyItem = Place.from(item) + placeFromNearbyItem.isMonument = false + places.add(placeFromNearbyItem) + } + return places + } + throw Exception(response.message) + } + + @Throws(Exception::class) + fun getNearbyPlaces( + screenTopRight: LatLng, + screenBottomLeft: LatLng, language: String, + shouldQueryForMonuments: Boolean, customQuery: String? + ): List? { + Timber.d("CUSTOM_SPARQL: %s", (customQuery != null).toString()) + + val wikidataQuery: String = if (customQuery != null) { + customQuery + } else if (!shouldQueryForMonuments) { + FileUtils.readFromResource("/queries/rectangle_query_for_nearby.rq") + } else { + FileUtils.readFromResource("/queries/rectangle_query_for_nearby_monuments.rq") + } + + val westCornerLat = screenTopRight.latitude + val westCornerLong = screenTopRight.longitude + val eastCornerLat = screenBottomLeft.latitude + val eastCornerLong = screenBottomLeft.longitude + + val query = wikidataQuery + .replace("\${LAT_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLat)) + .replace("\${LONG_WEST}", String.format(Locale.ROOT, "%.4f", westCornerLong)) + .replace("\${LAT_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLat)) + .replace("\${LONG_EAST}", String.format(Locale.ROOT, "%.4f", eastCornerLong)) + .replace("\${LANG}", language) + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder() + .url(urlBuilder.build()) + .build() + + val response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json = response.body!!.string() + val nearbyResponse = gson.fromJson(json, NearbyResponse::class.java) + val bindings = nearbyResponse.results.bindings + val places: MutableList = ArrayList() + for (item in bindings) { + val placeFromNearbyItem = Place.from(item) + if (shouldQueryForMonuments && item.getMonument() != null) { + placeFromNearbyItem.isMonument = true + } else { + placeFromNearbyItem.isMonument = false + } + places.add(placeFromNearbyItem) + } + return places + } + throw Exception(response.message) + } + + @Throws(IOException::class) + fun getPlaces( + placeList: List, language: String + ): List? { + val wikidataQuery = FileUtils.readFromResource("/queries/query_for_item.rq") + var qids = "" + for (place in placeList) { + qids += """ +${"wd:" + place.wikiDataEntityId}""" + } + val query = wikidataQuery + .replace("\${ENTITY}", qids) + .replace("\${LANG}", language) + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder().url(urlBuilder.build()).build() + + okHttpClient.newCall(request).execute().use { response -> + if (response.isSuccessful) { + val json = response.body!!.string() + val nearbyResponse = gson.fromJson(json, NearbyResponse::class.java) + val bindings = nearbyResponse.results.bindings + val places: MutableList = ArrayList() + for (item in bindings) { + val placeFromNearbyItem = Place.from(item) + places.add(placeFromNearbyItem) + } + return places + } else { + throw IOException("Unexpected response code: " + response.code) + } + } + } + + @Throws(Exception::class) + fun getPlacesAsKML(leftLatLng: LatLng, rightLatLng: LatLng): String? { + var kmlString = """ + + + """ + val placeBindings = runQuery( + leftLatLng, + rightLatLng + ) + if (placeBindings != null) { + for ((item1, label, location, clas) in placeBindings) { + if (item1 != null && label != null && clas != null) { + val input = location.value + val pattern = Pattern.compile( + "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)" + ) + val matcher = pattern.matcher(input) + + if (matcher.find()) { + val longStr = matcher.group(1) + val latStr = matcher.group(2) + val itemUrl = item1.value + val itemName = label.value.replace("&", "&") + val itemLatitude = latStr + val itemLongitude = longStr + val itemClass = clas.value + + val formattedItemName = + if (!itemClass.isEmpty()) + "$itemName ($itemClass)" + else + itemName + + val kmlEntry = (""" + + $formattedItemName + $itemUrl + + $itemLongitude,$itemLatitude + + """) + kmlString = kmlString + kmlEntry + } else { + Timber.e("No match found") + } + } + } + } + kmlString = """$kmlString + + +""" + return kmlString + } + + @Throws(Exception::class) + fun getPlacesAsGPX(leftLatLng: LatLng, rightLatLng: LatLng): String? { + var gpxString = (""" + +""") + + val placeBindings = runQuery(leftLatLng, rightLatLng) + if (placeBindings != null) { + for ((item1, label, location, clas) in placeBindings) { + if (item1 != null && label != null && clas != null) { + val input = location.value + val pattern = Pattern.compile( + "Point\\(([-+]?[0-9]*\\.?[0-9]+) ([-+]?[0-9]*\\.?[0-9]+)\\)" + ) + val matcher = pattern.matcher(input) + + if (matcher.find()) { + val longStr = matcher.group(1) + val latStr = matcher.group(2) + val itemUrl = item1.value + val itemName = label.value.replace("&", "&") + val itemLatitude = latStr + val itemLongitude = longStr + val itemClass = clas.value + + val formattedItemName = if (!itemClass.isEmpty()) + "$itemName ($itemClass)" + else + itemName + + val gpxEntry = + (""" + + $itemName + $itemUrl + """) + gpxString = gpxString + gpxEntry + } else { + Timber.e("No match found") + } + } + } + } + gpxString = "$gpxString\n" + return gpxString + } + + @Throws(IOException::class) + fun getChildDepictions( + qid: String, startPosition: Int, + limit: Int + ): Single> = + depictedItemsFrom(sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq")) + + @Throws(IOException::class) + fun getParentDepictions( + qid: String, startPosition: Int, + limit: Int + ): Single> = depictedItemsFrom( + sparqlQuery( + qid, + startPosition, + limit, + "/queries/parentclasses_query.rq" + ) + ) + + fun getCampaigns(): Single { + return Single.fromCallable({ + val request: Request = Request.Builder().url(campaignsUrl).build() + val response: Response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json: String = response.body!!.string() + return@fromCallable gson.fromJson( + json, + CampaignResponseDTO::class.java + ) + } + null + }) + } + + private fun depictedItemsFrom(request: Request): Single> { + return depictsClient.toDepictions(Single.fromCallable({ + okHttpClient.newCall(request).execute().body.use { body -> + return@fromCallable gson.fromJson( + body!!.string(), + SparqlResponse::class.java + ) + } + }).doOnError({ t: Throwable? -> Timber.e(t) })) + } + + @Throws(IOException::class) + private fun sparqlQuery( + qid: String, + startPosition: Int, + limit: Int, + fileName: String + ): Request { + val query = FileUtils.readFromResource(fileName) + .replace("\${QID}", qid) + .replace("\${LANG}", "\"" + Locale.getDefault().language + "\"") + .replace("\${LIMIT}", "" + limit) + .replace("\${OFFSET}", "" + startPosition) + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + return Request.Builder().url(urlBuilder.build()).build() + } + + @Throws(IOException::class) + private fun runQuery(currentLatLng: LatLng, nextLatLng: LatLng): List? { + val wikidataQuery = FileUtils.readFromResource("/queries/places_query.rq") + val query = wikidataQuery + .replace("\${LONGITUDE}", String.format(Locale.ROOT, "%.2f", currentLatLng.longitude)) + .replace("\${LATITUDE}", String.format(Locale.ROOT, "%.4f", currentLatLng.latitude)) + .replace("\${NEXT_LONGITUDE}", String.format(Locale.ROOT, "%.4f", nextLatLng.longitude)) + .replace("\${NEXT_LATITUDE}", String.format(Locale.ROOT, "%.4f", nextLatLng.latitude)) + + val urlBuilder: HttpUrl.Builder = sparqlQueryUrl.toHttpUrlOrNull()!! + .newBuilder() + .addQueryParameter("query", query) + .addQueryParameter("format", "json") + + val request: Request = Request.Builder().url(urlBuilder.build()).build() + + val response = okHttpClient.newCall(request).execute() + if (response.body != null && response.isSuccessful) { + val json = response.body!!.string() + val item = gson.fromJson(json, ItemsClass::class.java) + return item.results.bindings + } else { + return null + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt index 68c6f13fbe..d51ab1796e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.kt @@ -194,7 +194,7 @@ class FileProcessor requireNotNull(imageCoordinates.decimalCoords) compositeDisposable.add( apiCall - .request(imageCoordinates.decimalCoords) + .request(imageCoordinates.decimalCoords!!) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.io()) .subscribe( @@ -220,7 +220,7 @@ class FileProcessor .concatMap { Observable.fromCallable { okHttpJsonApiClient.getNearbyPlaces( - imageCoordinates.latLng, + imageCoordinates.latLng!!, Locale.getDefault().language, it, ) diff --git a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 00cd29a6d9..ae2c461f83 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt @@ -496,14 +496,14 @@ class UploadWorker( withContext(Dispatchers.Main) { wikidataEditService.handleImageClaimResult( - contribution.wikidataPlace, + contribution.wikidataPlace!!, revisionID, ) } } else { withContext(Dispatchers.Main) { wikidataEditService.handleImageClaimResult( - contribution.wikidataPlace, + contribution.wikidataPlace!!, null, ) } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt index ca523a21fd..bc0ba24fa1 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/CommonsServiceFactory.kt @@ -10,11 +10,10 @@ class CommonsServiceFactory( ) { val builder: Retrofit.Builder by lazy { // All instances of retrofit share this configuration, but create it lazily - Retrofit - .Builder() + Retrofit.Builder() .client(okHttpClient) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) - .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) + .addConverterFactory(GsonConverterFactory.create(GsonUtil.defaultGson)) } val retrofitCache: MutableMap = mutableMapOf() diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java b/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java deleted file mode 100644 index c9d37eda5c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -import android.net.Uri; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory; -import fr.free.nrw.commons.wikidata.model.DataValue; -import fr.free.nrw.commons.wikidata.model.WikiSite; -import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter; -import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter; -import fr.free.nrw.commons.wikidata.json.UriTypeAdapter; -import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter; -import fr.free.nrw.commons.wikidata.model.page.Namespace; - -public final class GsonUtil { - private static final String DATE_FORMAT = "MMM dd, yyyy HH:mm:ss"; - - private static final GsonBuilder DEFAULT_GSON_BUILDER = new GsonBuilder() - .setDateFormat(DATE_FORMAT) - .registerTypeAdapterFactory(DataValue.getPolymorphicTypeAdapter()) - .registerTypeHierarchyAdapter(Uri.class, new UriTypeAdapter().nullSafe()) - .registerTypeHierarchyAdapter(Namespace.class, new NamespaceTypeAdapter().nullSafe()) - .registerTypeAdapter(WikiSite.class, new WikiSiteTypeAdapter().nullSafe()) - .registerTypeAdapterFactory(new RequiredFieldsCheckOnReadTypeAdapterFactory()) - .registerTypeAdapterFactory(new PostProcessingTypeAdapter()); - - private static final Gson DEFAULT_GSON = DEFAULT_GSON_BUILDER.create(); - - public static Gson getDefaultGson() { - return DEFAULT_GSON; - } - - private GsonUtil() { } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt new file mode 100644 index 0000000000..1a0ae0aebc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/GsonUtil.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.wikidata + +import android.net.Uri +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import fr.free.nrw.commons.wikidata.json.NamespaceTypeAdapter +import fr.free.nrw.commons.wikidata.json.PostProcessingTypeAdapter +import fr.free.nrw.commons.wikidata.json.RequiredFieldsCheckOnReadTypeAdapterFactory +import fr.free.nrw.commons.wikidata.json.UriTypeAdapter +import fr.free.nrw.commons.wikidata.json.WikiSiteTypeAdapter +import fr.free.nrw.commons.wikidata.model.DataValue.Companion.polymorphicTypeAdapter +import fr.free.nrw.commons.wikidata.model.WikiSite +import fr.free.nrw.commons.wikidata.model.page.Namespace + +object GsonUtil { + private const val DATE_FORMAT = "MMM dd, yyyy HH:mm:ss" + + private val DEFAULT_GSON_BUILDER: GsonBuilder by lazy { + GsonBuilder().setDateFormat(DATE_FORMAT) + .registerTypeAdapterFactory(polymorphicTypeAdapter) + .registerTypeHierarchyAdapter(Uri::class.java, UriTypeAdapter().nullSafe()) + .registerTypeHierarchyAdapter(Namespace::class.java, NamespaceTypeAdapter().nullSafe()) + .registerTypeAdapter(WikiSite::class.java, WikiSiteTypeAdapter().nullSafe()) + .registerTypeAdapterFactory(RequiredFieldsCheckOnReadTypeAdapterFactory()) + .registerTypeAdapterFactory(PostProcessingTypeAdapter()) + } + + val defaultGson: Gson by lazy { DEFAULT_GSON_BUILDER.create() } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java deleted file mode 100644 index f89b5aee01..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java +++ /dev/null @@ -1,11 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -public class WikidataConstants { - public static final String PLACE_OBJECT = "place"; - public static final String BOOKMARKS_ITEMS = "bookmarks.items"; - public static final String SELECTED_NEARBY_PLACE = "selected.nearby.place"; - public static final String SELECTED_NEARBY_PLACE_CATEGORY = "selected.nearby.place.category"; - - public static final String MW_API_PREFIX = "w/api.php?format=json&formatversion=2&errorformat=plaintext&"; - public static final String WIKIPEDIA_URL = "https://wikipedia.org/"; -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt new file mode 100644 index 0000000000..6343342cb3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.kt @@ -0,0 +1,11 @@ +package fr.free.nrw.commons.wikidata + +object WikidataConstants { + const val PLACE_OBJECT: String = "place" + const val BOOKMARKS_ITEMS: String = "bookmarks.items" + const val SELECTED_NEARBY_PLACE: String = "selected.nearby.place" + const val SELECTED_NEARBY_PLACE_CATEGORY: String = "selected.nearby.place.category" + + const val MW_API_PREFIX: String = "w/api.php?format=json&formatversion=2&errorformat=plaintext&" + const val WIKIPEDIA_URL: String = "https://wikipedia.org/" +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java deleted file mode 100644 index 30fb26ddc7..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -public abstract class WikidataEditListener { - - protected WikidataP18EditListener wikidataP18EditListener; - - public abstract void onSuccessfulWikidataEdit(); - - public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) { - this.wikidataP18EditListener = wikidataP18EditListener; - } - - public interface WikidataP18EditListener { - void onWikidataEditSuccessful(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt new file mode 100644 index 0000000000..5e382b4ce8 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.kt @@ -0,0 +1,11 @@ +package fr.free.nrw.commons.wikidata + +abstract class WikidataEditListener { + var authenticationStateListener: WikidataP18EditListener? = null + + abstract fun onSuccessfulWikidataEdit() + + interface WikidataP18EditListener { + fun onWikidataEditSuccessful() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java deleted file mode 100644 index a97d0ededf..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java +++ /dev/null @@ -1,20 +0,0 @@ -package fr.free.nrw.commons.wikidata; - -/** - * Listener for wikidata edits - */ -public class WikidataEditListenerImpl extends WikidataEditListener { - - public WikidataEditListenerImpl() { - } - - /** - * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired - */ - @Override - public void onSuccessfulWikidataEdit() { - if (wikidataP18EditListener != null) { - wikidataP18EditListener.onWikidataEditSuccessful(); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt new file mode 100644 index 0000000000..6827ab30cc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.kt @@ -0,0 +1,13 @@ +package fr.free.nrw.commons.wikidata + +/** + * Listener for wikidata edits + */ +class WikidataEditListenerImpl : WikidataEditListener() { + /** + * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired + */ + override fun onSuccessfulWikidataEdit() { + authenticationStateListener?.onWikidataEditSuccessful() + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java deleted file mode 100644 index 21567f5e44..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java +++ /dev/null @@ -1,271 +0,0 @@ -package fr.free.nrw.commons.wikidata; - - -import static fr.free.nrw.commons.media.MediaClientKt.PAGE_ID_PREFIX; - -import android.annotation.SuppressLint; -import android.content.Context; -import androidx.annotation.Nullable; -import com.google.gson.Gson; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.kvstore.JsonKvStore; -import fr.free.nrw.commons.upload.UploadResult; -import fr.free.nrw.commons.upload.WikidataItem; -import fr.free.nrw.commons.upload.WikidataPlace; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.ViewUtil; -import fr.free.nrw.commons.wikidata.model.DataValue; -import fr.free.nrw.commons.wikidata.model.DataValue.ValueString; -import fr.free.nrw.commons.wikidata.model.EditClaim; -import fr.free.nrw.commons.wikidata.model.RemoveClaim; -import fr.free.nrw.commons.wikidata.model.SnakPartial; -import fr.free.nrw.commons.wikidata.model.StatementPartial; -import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue; -import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse; -import io.reactivex.Observable; -import io.reactivex.schedulers.Schedulers; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.UUID; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import timber.log.Timber; - -/** - * This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki - * Apis to make the necessary calls, log the edits and fire listeners on successful edits - */ -@Singleton -public class WikidataEditService { - - public static final String COMMONS_APP_TAG = "wikimedia-commons-app"; - - private final Context context; - private final WikidataEditListener wikidataEditListener; - private final JsonKvStore directKvStore; - private final WikiBaseClient wikiBaseClient; - private final WikidataClient wikidataClient; - private final Gson gson; - - @Inject - public WikidataEditService(final Context context, - final WikidataEditListener wikidataEditListener, - @Named("default_preferences") final JsonKvStore directKvStore, - final WikiBaseClient wikiBaseClient, - final WikidataClient wikidataClient, final Gson gson) { - this.context = context; - this.wikidataEditListener = wikidataEditListener; - this.directKvStore = directKvStore; - this.wikiBaseClient = wikiBaseClient; - this.wikidataClient = wikidataClient; - this.gson = gson; - } - - /** - * Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call - * to the wikibase API to set tag against the entity. - */ - @SuppressLint("CheckResult") - private Observable addDepictsProperty( - final String fileEntityId, - final List depictedItems - ) { - final EditClaim data = editClaim( - ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10") - // Wikipedia:Sandbox (Q10) - : depictedItems - ); - - return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) - .doOnNext(success -> { - if (success) { - Timber.d("DEPICTS property was set successfully for %s", fileEntityId); - } else { - Timber.d("Unable to set DEPICTS property for %s", fileEntityId); - } - }) - .doOnError(throwable -> { - Timber.e(throwable, "Error occurred while setting DEPICTS property"); - ViewUtil.showLongToast(context, throwable.toString()); - }) - .subscribeOn(Schedulers.io()); - } - - /** - * Takes depicts ID as a parameter and create a uploadable data with the Id - * and send the data for POST operation - * - * @param fileEntityId ID of the file - * @param depictedItems IDs of the selected depict item - * @return Observable - */ - @SuppressLint("CheckResult") - public Observable updateDepictsProperty( - final String fileEntityId, - final List depictedItems - ) { - final String entityId = PAGE_ID_PREFIX + fileEntityId; - final List claimIds = getDepictionsClaimIds(entityId); - - final RemoveClaim data = removeClaim( /* Please consider removeClaim scenario for BetaDebug */ - ConfigUtils.isBetaFlavour() ? Collections.singletonList("Q10") - // Wikipedia:Sandbox (Q10) - : claimIds - ); - - return wikiBaseClient.postDeleteClaims(entityId, gson.toJson(data)) - .doOnError(throwable -> { - Timber.e(throwable, "Error occurred while removing existing claims for DEPICTS property"); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - }).switchMap(success-> { - if(success) { - Timber.d("DEPICTS property was deleted successfully"); - return addDepictsProperty(fileEntityId, depictedItems); - } else { - Timber.d("Unable to delete DEPICTS property"); - return Observable.empty(); - } - }); - } - - @SuppressLint("CheckResult") - private List getDepictionsClaimIds(final String entityId) { - return wikiBaseClient.getClaimIdsByProperty(entityId, WikidataProperties.DEPICTS.getPropertyName()) - .subscribeOn(Schedulers.io()) - .blockingFirst(); - } - - private EditClaim editClaim(final List entityIds) { - return EditClaim.from(entityIds, WikidataProperties.DEPICTS.getPropertyName()); - } - - private RemoveClaim removeClaim(final List claimIds) { - return RemoveClaim.from(claimIds); - } - - /** - * Show a success toast when the edit is made successfully - */ - private void showSuccessToast(final String wikiItemName) { - final String successStringTemplate = context.getString(R.string.successful_wikidata_edit); - final String successMessage = String - .format(Locale.getDefault(), successStringTemplate, wikiItemName); - ViewUtil.showLongToast(context, successMessage); - } - - /** - * Adds label to Wikidata using the fileEntityId and the edit token, obtained from - * csrfTokenClient - * - * @param fileEntityId - * @return - */ - @SuppressLint("CheckResult") - private Observable addCaption(final long fileEntityId, final String languageCode, - final String captionValue) { - return wikiBaseClient.addLabelsToWikidata(fileEntityId, languageCode, captionValue) - .doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse)) - .doOnError(throwable -> { - Timber.e(throwable, "Error occurred while setting Captions"); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - }) - .map(mwPostResponse -> mwPostResponse != null); - } - - private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) { - if (response != null) { - Timber.d("Caption successfully set, revision id = %s", response); - } else { - Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId); - } - } - - public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, - final Map captions) { - if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) { - Timber - .d("Image location and nearby place location mismatched, so Wikidata item won't be edited"); - return null; - } - return addImageAndMediaLegends(wikidataPlace, fileName, captions); - } - - public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName, - final Map captions) { - final SnakPartial p18 = new SnakPartial("value", - WikidataProperties.IMAGE.getPropertyName(), - new ValueString(fileName.replace("File:", ""))); - - final List snaks = new ArrayList<>(); - for (final Map.Entry entry : captions.entrySet()) { - snaks.add(new SnakPartial("value", - WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText( - new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey())))); - } - - final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString(); - final StatementPartial claim = new StatementPartial(p18, "statement", "normal", id, - Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks), - Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName())); - - return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle(); - } - - public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) { - if (revisionId != null) { - if (wikidataEditListener != null) { - wikidataEditListener.onSuccessfulWikidataEdit(); - } - showSuccessToast(wikidataItem.getName()); - } else { - Timber.d("Unable to make wiki data edit for entity %s", wikidataItem); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - } - } - - public Observable addDepictionsAndCaptions( - final UploadResult uploadResult, - final Contribution contribution - ) { - return wikiBaseClient.getFileEntityId(uploadResult) - .doOnError(throwable -> { - Timber - .e(throwable, "Error occurred while getting EntityID to set DEPICTS property"); - ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); - }) - .switchMap(fileEntityId -> { - if (fileEntityId != null) { - Timber.d("EntityId for image was received successfully: %s", fileEntityId); - return Observable.concat( - depictionEdits(contribution, fileEntityId), - captionEdits(contribution, fileEntityId) - ); - } else { - Timber.d("Error acquiring EntityId for image: %s", uploadResult); - return Observable.empty(); - } - } - ); - } - - private Observable captionEdits(Contribution contribution, Long fileEntityId) { - return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet()) - .concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue())); - } - - private Observable depictionEdits(Contribution contribution, Long fileEntityId) { - final List depictIDs = new ArrayList<>(); - for (final WikidataItem wikidataItem : - contribution.getDepictedItems()) { - depictIDs.add(wikidataItem.getId()); - } - return addDepictsProperty(fileEntityId.toString(), depictIDs); - } -} - diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt new file mode 100644 index 0000000000..396f928245 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.kt @@ -0,0 +1,252 @@ +package fr.free.nrw.commons.wikidata + +import android.annotation.SuppressLint +import android.content.Context +import com.google.gson.Gson +import fr.free.nrw.commons.R +import fr.free.nrw.commons.contributions.Contribution +import fr.free.nrw.commons.kvstore.JsonKvStore +import fr.free.nrw.commons.media.PAGE_ID_PREFIX +import fr.free.nrw.commons.upload.UploadResult +import fr.free.nrw.commons.upload.WikidataItem +import fr.free.nrw.commons.upload.WikidataPlace +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import fr.free.nrw.commons.wikidata.WikidataProperties.DEPICTS +import fr.free.nrw.commons.wikidata.WikidataProperties.IMAGE +import fr.free.nrw.commons.wikidata.WikidataProperties.MEDIA_LEGENDS +import fr.free.nrw.commons.wikidata.model.DataValue.MonoLingualText +import fr.free.nrw.commons.wikidata.model.DataValue.ValueString +import fr.free.nrw.commons.wikidata.model.EditClaim +import fr.free.nrw.commons.wikidata.model.RemoveClaim +import fr.free.nrw.commons.wikidata.model.SnakPartial +import fr.free.nrw.commons.wikidata.model.StatementPartial +import fr.free.nrw.commons.wikidata.model.WikiBaseMonolingualTextValue +import fr.free.nrw.commons.wikidata.mwapi.MwPostResponse +import io.reactivex.Observable +import io.reactivex.schedulers.Schedulers +import timber.log.Timber +import java.util.Arrays +import java.util.Collections +import java.util.Locale +import java.util.Objects +import java.util.UUID +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + + +/** + * This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki + * Apis to make the necessary calls, log the edits and fire listeners on successful edits + */ +@Singleton +class WikidataEditService @Inject constructor( + private val context: Context, + private val wikidataEditListener: WikidataEditListener?, + @param:Named("default_preferences") private val directKvStore: JsonKvStore, + private val wikiBaseClient: WikiBaseClient, + private val wikidataClient: WikidataClient, private val gson: Gson +) { + @SuppressLint("CheckResult") + private fun addDepictsProperty( + fileEntityId: String, + depictedItems: List + ): Observable { + val data = EditClaim.from( + if (isBetaFlavour) listOf("Q10") else depictedItems, DEPICTS.propertyName + ) + + return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data)) + .doOnNext { success: Boolean -> + if (success) { + Timber.d("DEPICTS property was set successfully for %s", fileEntityId) + } else { + Timber.d("Unable to set DEPICTS property for %s", fileEntityId) + } + } + .doOnError { throwable: Throwable -> + Timber.e(throwable, "Error occurred while setting DEPICTS property") + showLongToast(context, throwable.toString()) + } + .subscribeOn(Schedulers.io()) + } + + @SuppressLint("CheckResult") + fun updateDepictsProperty( + fileEntityId: String?, + depictedItems: List + ): Observable { + val entityId: String = PAGE_ID_PREFIX + fileEntityId + val claimIds = getDepictionsClaimIds(entityId) + + /* Please consider removeClaim scenario for BetaDebug */ + val data = RemoveClaim.from(if (isBetaFlavour) listOf("Q10") else claimIds) + + return wikiBaseClient.postDeleteClaims(entityId, gson.toJson(data)) + .doOnError { throwable: Throwable? -> + Timber.e( + throwable, + "Error occurred while removing existing claims for DEPICTS property" + ) + showLongToast( + context, + context.getString(R.string.wikidata_edit_failure) + ) + }.switchMap { success: Boolean -> + if (success) { + Timber.d("DEPICTS property was deleted successfully") + return@switchMap addDepictsProperty(fileEntityId!!, depictedItems) + } else { + Timber.d("Unable to delete DEPICTS property") + return@switchMap Observable.empty() + } + } + } + + @SuppressLint("CheckResult") + private fun getDepictionsClaimIds(entityId: String): List { + return wikiBaseClient.getClaimIdsByProperty(entityId, DEPICTS.propertyName) + .subscribeOn(Schedulers.io()) + .blockingFirst() + } + + private fun showSuccessToast(wikiItemName: String) { + val successStringTemplate = context.getString(R.string.successful_wikidata_edit) + val successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName) + showLongToast(context, successMessage) + } + + @SuppressLint("CheckResult") + private fun addCaption( + fileEntityId: Long, languageCode: String, + captionValue: String + ): Observable { + return wikiBaseClient.addLabelsToWikidata(fileEntityId, languageCode, captionValue) + .doOnNext { mwPostResponse: MwPostResponse? -> + onAddCaptionResponse( + fileEntityId, + mwPostResponse + ) + } + .doOnError { throwable: Throwable? -> + Timber.e(throwable, "Error occurred while setting Captions") + showLongToast( + context, + context.getString(R.string.wikidata_edit_failure) + ) + } + .map(Objects::nonNull) + } + + private fun onAddCaptionResponse(fileEntityId: Long, response: MwPostResponse?) { + if (response != null) { + Timber.d("Caption successfully set, revision id = %s", response) + } else { + Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId) + } + } + + fun createClaim( + wikidataPlace: WikidataPlace?, fileName: String, + captions: Map + ): Long? { + if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) { + Timber.d( + "Image location and nearby place location mismatched, so Wikidata item won't be edited" + ) + return null + } + return addImageAndMediaLegends(wikidataPlace!!, fileName, captions) + } + + fun addImageAndMediaLegends( + wikidataItem: WikidataItem, fileName: String, + captions: Map + ): Long { + val p18 = SnakPartial( + "value", + IMAGE.propertyName, + ValueString(fileName.replace("File:", "")) + ) + + val snaks: MutableList = ArrayList() + for ((key, value) in captions) { + snaks.add( + SnakPartial( + "value", + MEDIA_LEGENDS.propertyName, MonoLingualText( + WikiBaseMonolingualTextValue(value!!, key!!) + ) + ) + ) + } + + val id = wikidataItem.id + "$" + UUID.randomUUID().toString() + val claim = StatementPartial( + p18, "statement", "normal", id, Collections.singletonMap>( + MEDIA_LEGENDS.propertyName, snaks + ), Arrays.asList(MEDIA_LEGENDS.propertyName) + ) + + return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle() + } + + fun handleImageClaimResult(wikidataItem: WikidataItem, revisionId: Long?) { + if (revisionId != null) { + wikidataEditListener?.onSuccessfulWikidataEdit() + showSuccessToast(wikidataItem.name) + } else { + Timber.d("Unable to make wiki data edit for entity %s", wikidataItem) + showLongToast(context, context.getString(R.string.wikidata_edit_failure)) + } + } + + fun addDepictionsAndCaptions( + uploadResult: UploadResult, + contribution: Contribution + ): Observable { + return wikiBaseClient.getFileEntityId(uploadResult) + .doOnError { throwable: Throwable? -> + Timber.e( + throwable, + "Error occurred while getting EntityID to set DEPICTS property" + ) + showLongToast( + context, + context.getString(R.string.wikidata_edit_failure) + ) + } + .switchMap { fileEntityId: Long? -> + if (fileEntityId != null) { + Timber.d("EntityId for image was received successfully: %s", fileEntityId) + return@switchMap Observable.concat( + depictionEdits(contribution, fileEntityId), + captionEdits(contribution, fileEntityId) + ) + } else { + Timber.d("Error acquiring EntityId for image: %s", uploadResult) + return@switchMap Observable.empty() + } + } + } + + private fun captionEdits(contribution: Contribution, fileEntityId: Long): Observable { + return Observable.fromIterable(contribution.media.captions.entries) + .concatMap { addCaption(fileEntityId, it.key, it.value) } + } + + private fun depictionEdits( + contribution: Contribution, + fileEntityId: Long + ): Observable = addDepictsProperty(fileEntityId.toString(), buildList { + for ((_, _, _, _, _, _, id) in contribution.depictedItems) { + add(id) + } + }) + + companion object { + const val COMMONS_APP_TAG: String = "wikimedia-commons-app" + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java deleted file mode 100644 index cc6dcc9f92..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.java +++ /dev/null @@ -1,29 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import fr.free.nrw.commons.wikidata.model.page.Namespace; - -import java.io.IOException; - -public class NamespaceTypeAdapter extends TypeAdapter { - - @Override - public void write(JsonWriter out, Namespace namespace) throws IOException { - out.value(namespace.code()); - } - - @Override - public Namespace read(JsonReader in) throws IOException { - if (in.peek() == JsonToken.STRING) { - // Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of - // the code number. This introduces a backwards-compatible check for the string value. - // TODO: remove after April 2017, when all older namespaces have been deserialized. - return Namespace.valueOf(in.nextString()); - } - return Namespace.of(in.nextInt()); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt new file mode 100644 index 0000000000..09f1dc5e85 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/NamespaceTypeAdapter.kt @@ -0,0 +1,26 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import fr.free.nrw.commons.wikidata.model.page.Namespace +import java.io.IOException + +class NamespaceTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, namespace: Namespace) { + out.value(namespace.code().toLong()) + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): Namespace { + if (reader.peek() == JsonToken.STRING) { + // Prior to 3210ce44, we marshaled Namespace as the name string of the enum, instead of + // the code number. This introduces a backwards-compatible check for the string value. + // TODO: remove after April 2017, when all older namespaces have been deserialized. + return Namespace.valueOf(reader.nextString()) + } + return Namespace.of(reader.nextInt()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java deleted file mode 100644 index b6b67d4d22..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; - -public class PostProcessingTypeAdapter implements TypeAdapterFactory { - public interface PostProcessable { - void postProcess(); - } - - public TypeAdapter create(Gson gson, TypeToken type) { - final TypeAdapter delegate = gson.getDelegateAdapter(this, type); - - return new TypeAdapter() { - public void write(JsonWriter out, T value) throws IOException { - delegate.write(out, value); - } - - public T read(JsonReader in) throws IOException { - T obj = delegate.read(in); - if (obj instanceof PostProcessable) { - ((PostProcessable)obj).postProcess(); - } - return obj; - } - }; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt new file mode 100644 index 0000000000..cf07eabf49 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/PostProcessingTypeAdapter.kt @@ -0,0 +1,35 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.Gson +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.io.IOException + +class PostProcessingTypeAdapter : TypeAdapterFactory { + interface PostProcessable { + fun postProcess() + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter { + val delegate = gson.getDelegateAdapter(this, type) + + return object : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: T) { + delegate.write(out, value) + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): T { + val obj = delegate.read(reader) + if (obj is PostProcessable) { + (obj as PostProcessable).postProcess() + } + return obj + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java deleted file mode 100644 index c01b9fe662..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.java +++ /dev/null @@ -1,94 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.collection.ArraySet; - -import com.google.gson.Gson; -import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import fr.free.nrw.commons.wikidata.json.annotations.Required; - -import java.io.IOException; -import java.lang.reflect.Field; -import java.util.Collections; -import java.util.Set; - -/** - * TypeAdapterFactory that provides TypeAdapters that return null values for objects that are - * missing fields annotated with @Required. - * - * BEWARE: This means that a List or other Collection of objects that have @Required fields can - * contain null elements after deserialization! - * - * TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements - * annotation and another corresponding TypeAdapter(Factory). - */ -public class RequiredFieldsCheckOnReadTypeAdapterFactory implements TypeAdapterFactory { - @Nullable @Override public final TypeAdapter create(@NonNull Gson gson, @NonNull TypeToken typeToken) { - Class rawType = typeToken.getRawType(); - Set requiredFields = collectRequiredFields(rawType); - - if (requiredFields.isEmpty()) { - return null; - } - - setFieldsAccessible(requiredFields, true); - return new Adapter<>(gson.getDelegateAdapter(this, typeToken), requiredFields); - } - - @NonNull private Set collectRequiredFields(@NonNull Class clazz) { - Field[] fields = clazz.getDeclaredFields(); - Set required = new ArraySet<>(); - for (Field field : fields) { - if (field.isAnnotationPresent(Required.class)) { - required.add(field); - } - } - return Collections.unmodifiableSet(required); - } - - private void setFieldsAccessible(Iterable fields, boolean accessible) { - for (Field field : fields) { - field.setAccessible(accessible); - } - } - - private static final class Adapter extends TypeAdapter { - @NonNull private final TypeAdapter delegate; - @NonNull private final Set requiredFields; - - private Adapter(@NonNull TypeAdapter delegate, @NonNull final Set requiredFields) { - this.delegate = delegate; - this.requiredFields = requiredFields; - } - - @Override public void write(JsonWriter out, T value) throws IOException { - delegate.write(out, value); - } - - @Override @Nullable public T read(JsonReader in) throws IOException { - T deserialized = delegate.read(in); - return allRequiredFieldsPresent(deserialized, requiredFields) ? deserialized : null; - } - - private boolean allRequiredFieldsPresent(@NonNull T deserialized, - @NonNull Set required) { - for (Field field : required) { - try { - if (field.get(deserialized) == null) { - return false; - } - } catch (IllegalArgumentException | IllegalAccessException e) { - throw new JsonParseException(e); - } - } - return true; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt new file mode 100644 index 0000000000..ec26e8345c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RequiredFieldsCheckOnReadTypeAdapterFactory.kt @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.Gson +import com.google.gson.JsonParseException +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import fr.free.nrw.commons.wikidata.json.annotations.Required +import java.io.IOException +import java.lang.reflect.Field + +/** + * TypeAdapterFactory that provides TypeAdapters that return null values for objects that are + * missing fields annotated with @Required. + * + * BEWARE: This means that a List or other Collection of objects that have @Required fields can + * contain null elements after deserialization! + * + * TODO: Handle null values in lists during deserialization, perhaps with a new @RequiredElements + * annotation and another corresponding TypeAdapter(Factory). + */ +class RequiredFieldsCheckOnReadTypeAdapterFactory : TypeAdapterFactory { + override fun create(gson: Gson, typeToken: TypeToken): TypeAdapter? { + val rawType: Class<*> = typeToken.rawType + val requiredFields = collectRequiredFields(rawType) + + if (requiredFields.isEmpty()) { + return null + } + + for (field in requiredFields) { + field.isAccessible = true + } + + return Adapter(gson.getDelegateAdapter(this, typeToken), requiredFields) + } + + private fun collectRequiredFields(clazz: Class<*>): Set = buildSet { + for (field in clazz.declaredFields) { + if (field.isAnnotationPresent(Required::class.java)) add(field) + } + } + + private class Adapter( + private val delegate: TypeAdapter, + private val requiredFields: Set + ) : TypeAdapter() { + + @Throws(IOException::class) + override fun write(out: JsonWriter, value: T?) = + delegate.write(out, value) + + @Throws(IOException::class) + override fun read(reader: JsonReader): T? = + if (allRequiredFieldsPresent(delegate.read(reader), requiredFields)) + delegate.read(reader) + else + null + + fun allRequiredFieldsPresent(deserialized: T, required: Set): Boolean { + for (field in required) { + try { + if (field[deserialized] == null) return false + } catch (e: IllegalArgumentException) { + throw JsonParseException(e) + } catch (e: IllegalAccessException) { + throw JsonParseException(e) + } + } + return true + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java deleted file mode 100644 index 828dfbd681..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.java +++ /dev/null @@ -1,280 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import android.util.Log; -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.Streams; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -/** - * Adapts values whose runtime type may differ from their declaration type. This - * is necessary when a field's type is not the same type that GSON should create - * when deserializing that field. For example, consider these types: - *
   {@code
- *   abstract class Shape {
- *     int x;
- *     int y;
- *   }
- *   class Circle extends Shape {
- *     int radius;
- *   }
- *   class Rectangle extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Diamond extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Drawing {
- *     Shape bottomShape;
- *     Shape topShape;
- *   }
- * }
- *

Without additional type information, the serialized JSON is ambiguous. Is - * the bottom shape in this drawing a rectangle or a diamond?

   {@code
- *   {
- *     "bottomShape": {
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * This class addresses this problem by adding type information to the - * serialized JSON and honoring that type information when the JSON is - * deserialized:
   {@code
- *   {
- *     "bottomShape": {
- *       "type": "Diamond",
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "type": "Circle",
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * Both the type field name ({@code "type"}) and the type labels ({@code - * "Rectangle"}) are configurable. - * - *

Registering Types

- * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field - * name to the {@link #of} factory method. If you don't supply an explicit type - * field name, {@code "type"} will be used.
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory
- *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
- * }
- * Next register all of your subtypes. Every subtype must be explicitly - * registered. This protects your application from injection attacks. If you - * don't supply an explicit type label, the type's simple name will be used. - *
   {@code
- *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
- *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
- *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
- * }
- * Finally, register the type adapter factory in your application's GSON builder: - *
   {@code
- *   Gson gson = new GsonBuilder()
- *       .registerTypeAdapterFactory(shapeAdapterFactory)
- *       .create();
- * }
- * Like {@code GsonBuilder}, this API supports chaining:
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
- *       .registerSubtype(Rectangle.class)
- *       .registerSubtype(Circle.class)
- *       .registerSubtype(Diamond.class);
- * }
- * - *

Serialization and deserialization

- * In order to serialize and deserialize a polymorphic object, - * you must specify the base type explicitly. - *
   {@code
- *   Diamond diamond = new Diamond();
- *   String json = gson.toJson(diamond, Shape.class);
- * }
- * And then: - *
   {@code
- *   Shape shape = gson.fromJson(json, Shape.class);
- * }
- */ -public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap>(); - private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); - private final boolean maintainType; - - private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; - this.maintainType = maintainType; - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - * {@code maintainType} flag decide if the type will be stored in pojo or not. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); - } - - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory(baseType, "type", false); - } - - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; - } - - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); - } - - public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() != baseType) { - return null; - } - - final Map> labelToDelegate - = new LinkedHashMap>(); - final Map, TypeAdapter> subtypeToDelegate - = new LinkedHashMap, TypeAdapter>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); - } - - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = Streams.parse(in); - JsonElement labelJsonElement; - if (maintainType) { - labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); - } else { - labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); - } - - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - - Log.e("RuntimeTypeAdapter", "cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype? " +jsonElement); - return null; - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - - if (maintainType) { - Streams.write(jsonObject, out); - return; - } - - JsonObject clone = new JsonObject(); - - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } - clone.add(typeFieldName, new JsonPrimitive(label)); - - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - Streams.write(clone, out); - } - }.nullSafe(); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt new file mode 100644 index 0000000000..87acc939f3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/RuntimeTypeAdapterFactory.kt @@ -0,0 +1,273 @@ +package fr.free.nrw.commons.wikidata.json + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonPrimitive +import com.google.gson.TypeAdapter +import com.google.gson.TypeAdapterFactory +import com.google.gson.internal.Streams +import com.google.gson.reflect.TypeToken +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import timber.log.Timber +import java.io.IOException + +/* +* Copyright (C) 2011 Google Inc. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +/** + * Adapts values whose runtime type may differ from their declaration type. This + * is necessary when a field's type is not the same type that GSON should create + * when deserializing that field. For example, consider these types: + *
   `abstract class Shape {
+ * int x;
+ * int y;
+ * }
+ * class Circle extends Shape {
+ * int radius;
+ * }
+ * class Rectangle extends Shape {
+ * int width;
+ * int height;
+ * }
+ * class Diamond extends Shape {
+ * int width;
+ * int height;
+ * }
+ * class Drawing {
+ * Shape bottomShape;
+ * Shape topShape;
+ * }
+`
* + * + * Without additional type information, the serialized JSON is ambiguous. Is + * the bottom shape in this drawing a rectangle or a diamond?
   `{
+ * "bottomShape": {
+ * "width": 10,
+ * "height": 5,
+ * "x": 0,
+ * "y": 0
+ * },
+ * "topShape": {
+ * "radius": 2,
+ * "x": 4,
+ * "y": 1
+ * }
+ * }`
+ * This class addresses this problem by adding type information to the + * serialized JSON and honoring that type information when the JSON is + * deserialized:
   `{
+ * "bottomShape": {
+ * "type": "Diamond",
+ * "width": 10,
+ * "height": 5,
+ * "x": 0,
+ * "y": 0
+ * },
+ * "topShape": {
+ * "type": "Circle",
+ * "radius": 2,
+ * "x": 4,
+ * "y": 1
+ * }
+ * }`
+ * Both the type field name (`"type"`) and the type labels (`"Rectangle"`) are configurable. + * + *

Registering Types

+ * Create a `RuntimeTypeAdapterFactory` by passing the base type and type field + * name to the [.of] factory method. If you don't supply an explicit type + * field name, `"type"` will be used.
   `RuntimeTypeAdapterFactory shapeAdapterFactory
+ * = RuntimeTypeAdapterFactory.of(Shape.class, "type");
+`
* + * Next register all of your subtypes. Every subtype must be explicitly + * registered. This protects your application from injection attacks. If you + * don't supply an explicit type label, the type's simple name will be used. + *
   `shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
+ * shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
+ * shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
+`
* + * Finally, register the type adapter factory in your application's GSON builder: + *
   `Gson gson = new GsonBuilder()
+ * .registerTypeAdapterFactory(shapeAdapterFactory)
+ * .create();
+`
* + * Like `GsonBuilder`, this API supports chaining:
   `RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
+ * .registerSubtype(Rectangle.class)
+ * .registerSubtype(Circle.class)
+ * .registerSubtype(Diamond.class);
+`
* + * + *

Serialization and deserialization

+ * In order to serialize and deserialize a polymorphic object, + * you must specify the base type explicitly. + *
   `Diamond diamond = new Diamond();
+ * String json = gson.toJson(diamond, Shape.class);
+`
* + * And then: + *
   `Shape shape = gson.fromJson(json, Shape.class);
+`
* + */ +class RuntimeTypeAdapterFactory( + baseType: Class<*>?, + typeFieldName: String?, + maintainType: Boolean +) : TypeAdapterFactory { + + private val baseType: Class<*> + private val typeFieldName: String + private val labelToSubtype = mutableMapOf>() + private val subtypeToLabel = mutableMapOf, String>() + private val maintainType: Boolean + + init { + if (typeFieldName == null || baseType == null) { + throw NullPointerException() + } + this.baseType = baseType + this.typeFieldName = typeFieldName + this.maintainType = maintainType + } + + /** + * Registers `type` identified by `label`. Labels are case + * sensitive. + * + * @throws IllegalArgumentException if either `type` or `label` + * have already been registered on this type adapter. + */ + fun registerSubtype(type: Class?, label: String?): RuntimeTypeAdapterFactory { + if (type == null || label == null) { + throw NullPointerException() + } + require(!(subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label))) { + "types and labels must be unique" + } + + labelToSubtype[label] = type + subtypeToLabel[type] = label + return this + } + + /** + * Registers `type` identified by its [simple][Class.getSimpleName]. Labels are case sensitive. + * + * @throws IllegalArgumentException if either `type` or its simple name + * have already been registered on this type adapter. + */ + fun registerSubtype(type: Class): RuntimeTypeAdapterFactory { + return registerSubtype(type, type.simpleName) + } + + override fun create(gson: Gson, type: TypeToken): TypeAdapter? { + if (type.rawType != baseType) { + return null + } + + val labelToDelegate = mutableMapOf>() + val subtypeToDelegate = mutableMapOf, TypeAdapter<*>>() + for ((key, value) in labelToSubtype) { + val delegate = gson.getDelegateAdapter( + this, TypeToken.get( + value + ) + ) + labelToDelegate[key] = delegate + subtypeToDelegate[value] = delegate + } + + return object : TypeAdapter() { + @Throws(IOException::class) + override fun read(reader: JsonReader): R? { + val jsonElement = Streams.parse(reader) + val labelJsonElement = if (maintainType) { + jsonElement.asJsonObject[typeFieldName] + } else { + jsonElement.asJsonObject.remove(typeFieldName) + } + + if (labelJsonElement == null) { + throw JsonParseException( + "cannot deserialize $baseType because it does not define a field named $typeFieldName" + ) + } + val label = labelJsonElement.asString + val delegate = labelToDelegate[label] as TypeAdapter? + if (delegate == null) { + Timber.tag("RuntimeTypeAdapter").e( + "cannot deserialize $baseType subtype named $label; did you forget to register a subtype? $jsonElement" + ) + return null + } + return delegate.fromJsonTree(jsonElement) + } + + @Throws(IOException::class) + override fun write(out: JsonWriter, value: R) { + val srcType: Class<*> = value::class.java.javaClass + val delegate = + subtypeToDelegate[srcType] as TypeAdapter? ?: throw JsonParseException( + "cannot serialize ${srcType.name}; did you forget to register a subtype?" + ) + + val jsonObject = delegate.toJsonTree(value).asJsonObject + if (maintainType) { + Streams.write(jsonObject, out) + return + } + + if (jsonObject.has(typeFieldName)) { + throw JsonParseException( + "cannot serialize ${srcType.name} because it already defines a field named $typeFieldName" + ) + } + val clone = JsonObject() + val label = subtypeToLabel[srcType] + clone.add(typeFieldName, JsonPrimitive(label)) + for ((key, value1) in jsonObject.entrySet()) { + clone.add(key, value1) + } + Streams.write(clone, out) + } + }.nullSafe() + } + + companion object { + /** + * Creates a new runtime type adapter using for `baseType` using `typeFieldName` as the type field name. Type field names are case sensitive. + * `maintainType` flag decide if the type will be stored in pojo or not. + */ + fun of( + baseType: Class, + typeFieldName: String, + maintainType: Boolean + ): RuntimeTypeAdapterFactory = + RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType) + + /** + * Creates a new runtime type adapter using for `baseType` using `typeFieldName` as the type field name. Type field names are case sensitive. + */ + fun of(baseType: Class, typeFieldName: String): RuntimeTypeAdapterFactory = + RuntimeTypeAdapterFactory(baseType, typeFieldName, false) + + /** + * Creates a new runtime type adapter for `baseType` using `"type"` as + * the type field name. + */ + fun of(baseType: Class): RuntimeTypeAdapterFactory = + RuntimeTypeAdapterFactory(baseType, "type", false) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java deleted file mode 100644 index 069e02f321..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import android.net.Uri; - -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; - -public class UriTypeAdapter extends TypeAdapter { - @Override - public void write(JsonWriter out, Uri value) throws IOException { - out.value(value.toString()); - } - - @Override - public Uri read(JsonReader in) throws IOException { - String url = in.nextString(); - return Uri.parse(url); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt new file mode 100644 index 0000000000..305cfa28a5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/UriTypeAdapter.kt @@ -0,0 +1,19 @@ +package fr.free.nrw.commons.wikidata.json + +import android.net.Uri +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonWriter +import java.io.IOException + +class UriTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: Uri) { + out.value(value.toString()) + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): Uri { + return Uri.parse(reader.nextString()) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java deleted file mode 100644 index c268d1e738..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.java +++ /dev/null @@ -1,63 +0,0 @@ -package fr.free.nrw.commons.wikidata.json; - -import android.net.Uri; - -import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import fr.free.nrw.commons.wikidata.model.WikiSite; - -import java.io.IOException; - -public class WikiSiteTypeAdapter extends TypeAdapter { - private static final String DOMAIN = "domain"; - private static final String LANGUAGE_CODE = "languageCode"; - - @Override public void write(JsonWriter out, WikiSite value) throws IOException { - out.beginObject(); - out.name(DOMAIN); - out.value(value.url()); - - out.name(LANGUAGE_CODE); - out.value(value.languageCode()); - out.endObject(); - } - - @Override public WikiSite read(JsonReader in) throws IOException { - // todo: legacy; remove in June 2018 - if (in.peek() == JsonToken.STRING) { - return new WikiSite(Uri.parse(in.nextString())); - } - - String domain = null; - String languageCode = null; - in.beginObject(); - while (in.hasNext()) { - String field = in.nextName(); - String val = in.nextString(); - switch (field) { - case DOMAIN: - domain = val; - break; - case LANGUAGE_CODE: - languageCode = val; - break; - default: break; - } - } - in.endObject(); - - if (domain == null) { - throw new JsonParseException("Missing domain"); - } - - // todo: legacy; remove in June 2018 - if (languageCode == null) { - return new WikiSite(domain); - } - return new WikiSite(domain, languageCode); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt new file mode 100644 index 0000000000..da5cb08024 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/WikiSiteTypeAdapter.kt @@ -0,0 +1,61 @@ +package fr.free.nrw.commons.wikidata.json + +import android.net.Uri +import com.google.gson.JsonParseException +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import fr.free.nrw.commons.wikidata.model.WikiSite +import java.io.IOException + +class WikiSiteTypeAdapter : TypeAdapter() { + @Throws(IOException::class) + override fun write(out: JsonWriter, value: WikiSite) { + out.beginObject() + out.name(DOMAIN) + out.value(value.url()) + + out.name(LANGUAGE_CODE) + out.value(value.languageCode()) + out.endObject() + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): WikiSite { + // todo: legacy; remove reader June 2018 + if (reader.peek() == JsonToken.STRING) { + return WikiSite(Uri.parse(reader.nextString())) + } + + var domain: String? = null + var languageCode: String? = null + reader.beginObject() + while (reader.hasNext()) { + val field = reader.nextName() + val value = reader.nextString() + when (field) { + DOMAIN -> domain = value + LANGUAGE_CODE -> languageCode = value + else -> {} + } + } + reader.endObject() + + if (domain == null) { + throw JsonParseException("Missing domain") + } + + // todo: legacy; remove reader June 2018 + return if (languageCode == null) { + WikiSite(domain) + } else { + WikiSite(domain, languageCode) + } + } + + companion object { + private const val DOMAIN = "domain" + private const val LANGUAGE_CODE = "languageCode" + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java b/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java deleted file mode 100644 index 98e12745b8..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.java +++ /dev/null @@ -1,21 +0,0 @@ -package fr.free.nrw.commons.wikidata.json.annotations; - - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; - -/** - * Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return - * an instantiated object. - * - * E.g.: @NonNull @Required private String title; - */ -@Documented -@Retention(RetentionPolicy.RUNTIME) -@Target(FIELD) -public @interface Required { -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt new file mode 100644 index 0000000000..189a3a42cd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/json/annotations/Required.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.wikidata.json.annotations + + +/** + * Annotate fields in Retrofit POJO classes with this to enforce their presence in order to return + * an instantiated object. + * + * E.g.: @NonNull @Required private String title; + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FIELD) +annotation class Required diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java index 2d1dbdf28a..929fe0d13a 100644 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/notifications/Notification.java @@ -148,7 +148,7 @@ public void setPrimary(@Nullable final JsonElement primary) { return null; } if (primaryLink == null && primary instanceof JsonObject) { - primaryLink = GsonUtil.getDefaultGson().fromJson(primary, Link.class); + primaryLink = GsonUtil.INSTANCE.getDefaultGson().fromJson(primary, Link.class); } return primaryLink; } diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java b/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java deleted file mode 100644 index 3ac9e39159..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.java +++ /dev/null @@ -1,34 +0,0 @@ -package fr.free.nrw.commons.wikidata.mwapi; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import java.util.Map; - - -public class UserInfo { - @NonNull private String name; - @NonNull private int id; - - //Block information - private int blockid; - private String blockedby; - private int blockedbyid; - private String blockreason; - private String blocktimestamp; - private String blockexpiry; - - // Object type is any JSON type. - @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") - @Nullable private Map options; - - public int id() { - return id; - } - - @NonNull - public String blockexpiry() { - if (blockexpiry != null) - return blockexpiry; - else return ""; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt new file mode 100644 index 0000000000..c9182a821f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/mwapi/UserInfo.kt @@ -0,0 +1,21 @@ +package fr.free.nrw.commons.wikidata.mwapi + +data class UserInfo( + val name: String = "", + val id: Int = 0, + + //Block information + val blockid: Int = 0, + val blockedby: String? = null, + val blockedbyid: Int = 0, + val blockreason: String? = null, + val blocktimestamp: String? = null, + val blockexpiry: String? = null, + + // Object type is any JSON type. + val options: Map? = null +) { + fun id(): Int = id + + fun blockexpiry(): String = blockexpiry ?: "" +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java b/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java index fd940c12fa..d9c8ad4fbb 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java +++ b/app/src/test/kotlin/fr/free/nrw/commons/MockWebServerTest.java @@ -69,7 +69,7 @@ protected void enqueueEmptyJson() { .baseUrl(url) .callbackExecutor(new ImmediateExecutor()) .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(GsonUtil.getDefaultGson())) + .addConverterFactory(GsonConverterFactory.create(GsonUtil.INSTANCE.getDefaultGson())) .build() .create(clazz); } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt index ec3ad82f1b..f876916b64 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/campaigns/CampaignsPresenterTest.kt @@ -49,13 +49,13 @@ class CampaignsPresenterTest { campaignsSingle = Single.just(campaignResponseDTO) campaignsPresenter = CampaignsPresenter(okHttpJsonApiClient, testScheduler, testScheduler) campaignsPresenter.onAttachView(view) - Mockito.`when`(okHttpJsonApiClient.campaigns).thenReturn(campaignsSingle) + Mockito.`when`(okHttpJsonApiClient.getCampaigns()).thenReturn(campaignsSingle) } @Test fun getCampaignsTestNoCampaigns() { campaignsPresenter.getCampaigns() - verify(okHttpJsonApiClient).campaigns + verify(okHttpJsonApiClient).getCampaigns() testScheduler.triggerActions() verify(view).showCampaigns(null) } @@ -77,7 +77,7 @@ class CampaignsPresenterTest { Mockito.`when`(campaign.endDate).thenReturn(endDateString) Mockito.`when`(campaign.startDate).thenReturn(startDateString) Mockito.`when`(campaignResponseDTO.campaigns).thenReturn(campaigns) - verify(okHttpJsonApiClient).campaigns + verify(okHttpJsonApiClient).getCampaigns() testScheduler.triggerActions() verify(view).showCampaigns(campaign) } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt index 52c7953ec5..9266783085 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/UserClientTest.kt @@ -30,8 +30,7 @@ class UserClientTest { @Test fun isUserBlockedFromCommonsForInfinitelyBlockedUser() { - val userInfo = Mockito.mock(UserInfo::class.java) - Mockito.`when`(userInfo.blockexpiry()).thenReturn("infinite") + val userInfo = UserInfo(blockexpiry = "infinite") val mwQueryResult = Mockito.mock(MwQueryResult::class.java) Mockito.`when`(mwQueryResult.userInfo()).thenReturn(userInfo) val mockResponse = Mockito.mock(MwQueryResponse::class.java) @@ -49,8 +48,7 @@ class UserClientTest { val currentDate = Date() val expiredDate = Date(currentDate.time + 10000) - val userInfo = Mockito.mock(UserInfo::class.java) - Mockito.`when`(userInfo.blockexpiry()).thenReturn(DateUtil.iso8601DateFormat(expiredDate)) + val userInfo = UserInfo(blockexpiry = DateUtil.iso8601DateFormat(expiredDate)) val mwQueryResult = Mockito.mock(MwQueryResult::class.java) Mockito.`when`(mwQueryResult.userInfo()).thenReturn(userInfo) val mockResponse = Mockito.mock(MwQueryResponse::class.java) @@ -65,8 +63,7 @@ class UserClientTest { @Test fun isUserBlockedFromCommonsForNeverBlockedUser() { - val userInfo = Mockito.mock(UserInfo::class.java) - Mockito.`when`(userInfo.blockexpiry()).thenReturn("") + val userInfo = UserInfo(blockexpiry = "") val mwQueryResult = Mockito.mock(MwQueryResult::class.java) Mockito.`when`(mwQueryResult.userInfo()).thenReturn(userInfo) val mockResponse = Mockito.mock(MwQueryResponse::class.java) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt index 6584550b06..7fb3ba8bde 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/nearby/NearbyParentFragmentUnitTest.kt @@ -325,7 +325,7 @@ class NearbyParentFragmentUnitTest { @Throws(Exception::class) fun testOnDestroy() { fragment.onDestroy() - verify(wikidataEditListener).setAuthenticationStateListener(null) + verify(wikidataEditListener).authenticationStateListener = null } @Test @Ignore diff --git a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt index e9451cd759..7d7c668a8e 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/notification/NotificationClientTest.kt @@ -120,26 +120,16 @@ class NotificationClientTest { ) = Notification().apply { setId(notificationId) - setTimestamp( - Notification.Timestamp().apply { - setUtciso8601(timestamp) - }, - ) + setTimestamp(Notification.Timestamp().apply { setUtciso8601(timestamp) }) + + contents = Notification.Contents().apply { + setCompactHeader(compactHeader) - contents = - Notification.Contents().apply { - setCompactHeader(compactHeader) - - links = - Notification.Links().apply { - setPrimary( - GsonUtil.getDefaultGson().toJsonTree( - Notification.Link().apply { - setUrl(primaryUrl) - }, - ), - ) - } + links = Notification.Links().apply { + setPrimary(GsonUtil.defaultGson.toJsonTree( + Notification.Link().apply { setUrl(primaryUrl) } + )) } + } } } From f8d519e8eb04f3de5897d1b05b08a9cb94c2614e Mon Sep 17 00:00:00 2001 From: Saifuddin Adenwala Date: Fri, 6 Dec 2024 14:01:40 +0530 Subject: [PATCH 050/231] Migrated filepicker from Java to Kotlin (#5997) * Rename .java to .kt * Migrated filepicker module from Java to Kotlin * Rename .java to .kt * Migrated filepicker module from Java to Kotlin * fix: test cases --- .../nrw/commons/filepicker/Constants.java | 23 - .../free/nrw/commons/filepicker/Costants.kt | 29 ++ .../commons/filepicker/DefaultCallback.java | 16 - .../nrw/commons/filepicker/DefaultCallback.kt | 12 + .../filepicker/ExtendedFileProvider.java | 7 - .../filepicker/ExtendedFileProvider.kt | 5 + .../nrw/commons/filepicker/FilePicker.java | 355 -------------- .../free/nrw/commons/filepicker/FilePicker.kt | 441 ++++++++++++++++++ .../filepicker/FilePickerConfiguration.java | 44 -- .../filepicker/FilePickerConfiguration.kt | 46 ++ .../filepicker/MimeTypeMapWrapper.java | 26 -- .../commons/filepicker/MimeTypeMapWrapper.kt | 24 + .../nrw/commons/filepicker/PickedFiles.java | 208 --------- .../nrw/commons/filepicker/PickedFiles.kt | 195 ++++++++ .../commons/filepicker/UploadableFile.java | 213 --------- .../nrw/commons/filepicker/UploadableFile.kt | 168 +++++++ .../nrw/commons/settings/SettingsFragment.kt | 15 +- .../nrw/commons/utils/CustomSelectorUtils.kt | 2 +- .../filepicker/ShadowFileProvider.java | 32 -- .../commons/filepicker/ShadowFileProvider.kt | 36 ++ .../nrw/commons/upload/UploadPresenterTest.kt | 2 +- 21 files changed, 970 insertions(+), 929 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java create mode 100644 app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt delete mode 100644 app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java create mode 100644 app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java deleted file mode 100644 index 97a16acc32..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java +++ /dev/null @@ -1,23 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -public interface Constants { - String DEFAULT_FOLDER_NAME = "CommonsContributions"; - - /** - * Provides the request codes for permission handling - */ - interface RequestCodes { - int LOCATION = 1; - int STORAGE = 2; - } - - /** - * Provides locations as string for corresponding operations - */ - interface BundleKeys { - String FOLDER_NAME = "fr.free.nrw.commons.folder_name"; - String ALLOW_MULTIPLE = "fr.free.nrw.commons.allow_multiple"; - String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos"; - String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images"; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt new file mode 100644 index 0000000000..e405a6d52c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Costants.kt @@ -0,0 +1,29 @@ +package fr.free.nrw.commons.filepicker + +interface Constants { + companion object { + const val DEFAULT_FOLDER_NAME = "CommonsContributions" + } + + /** + * Provides the request codes for permission handling + */ + interface RequestCodes { + companion object { + const val LOCATION = 1 + const val STORAGE = 2 + } + } + + /** + * Provides locations as string for corresponding operations + */ + interface BundleKeys { + companion object { + const val FOLDER_NAME = "fr.free.nrw.commons.folder_name" + const val ALLOW_MULTIPLE = "fr.free.nrw.commons.allow_multiple" + const val COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos" + const val COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images" + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java deleted file mode 100644 index e8373dc6fa..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.java +++ /dev/null @@ -1,16 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -/** - * Provides abstract methods which are overridden while handling Contribution Results - * inside the ContributionsController - */ -public abstract class DefaultCallback implements FilePicker.Callbacks { - - @Override - public void onImagePickerError(Exception e, FilePicker.ImageSource source, int type) { - } - - @Override - public void onCanceled(FilePicker.ImageSource source, int type) { - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt new file mode 100644 index 0000000000..baaba67b5d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/DefaultCallback.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.filepicker + +/** + * Provides abstract methods which are overridden while handling Contribution Results + * inside the ContributionsController + */ +abstract class DefaultCallback: FilePicker.Callbacks { + + override fun onImagePickerError(e: Exception, source: FilePicker.ImageSource, type: Int) {} + + override fun onCanceled(source: FilePicker.ImageSource, type: Int) {} +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java deleted file mode 100644 index af3dc8622e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import androidx.core.content.FileProvider; - -public class ExtendedFileProvider extends FileProvider { - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt new file mode 100644 index 0000000000..746058fc43 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/ExtendedFileProvider.kt @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.filepicker + +import androidx.core.content.FileProvider + +class ExtendedFileProvider: FileProvider() {} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java deleted file mode 100644 index b64db24c5f..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.java +++ /dev/null @@ -1,355 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import static fr.free.nrw.commons.filepicker.PickedFiles.singleFileList; - -import android.app.Activity; -import android.content.ClipData; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.net.Uri; -import android.provider.MediaStore; -import android.text.TextUtils; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.preference.PreferenceManager; -import fr.free.nrw.commons.customselector.model.Image; -import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity; -import java.io.File; -import java.io.IOException; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.List; - -public class FilePicker implements Constants { - - private static final String KEY_PHOTO_URI = "photo_uri"; - private static final String KEY_VIDEO_URI = "video_uri"; - private static final String KEY_LAST_CAMERA_PHOTO = "last_photo"; - private static final String KEY_LAST_CAMERA_VIDEO = "last_video"; - private static final String KEY_TYPE = "type"; - - /** - * Returns the uri of the clicked image so that it can be put in MediaStore - */ - private static Uri createCameraPictureFile(@NonNull Context context) throws IOException { - File imagePath = PickedFiles.getCameraPicturesLocation(context); - Uri uri = PickedFiles.getUriToFile(context, imagePath); - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - editor.putString(KEY_PHOTO_URI, uri.toString()); - editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString()); - editor.apply(); - return uri; - } - - private static Intent createGalleryIntent(@NonNull Context context, int type, - boolean openDocumentIntentPreferred) { - // storing picked image type to shared preferences - storeType(context, type); - //Supported types are SVG, PNG and JPEG,GIF, TIFF, WebP, XCF - final String[] mimeTypes = { "image/jpg","image/png","image/jpeg", "image/gif", "image/tiff", "image/webp", "image/xcf", "image/svg+xml", "image/webp"}; - return plainGalleryPickerIntent(openDocumentIntentPreferred) - .putExtra(Intent.EXTRA_ALLOW_MULTIPLE, configuration(context).allowsMultiplePickingInGallery()) - .putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes); - } - - /** - * CreateCustomSectorIntent, creates intent for custom selector activity. - * @param context - * @param type - * @return Custom selector intent - */ - private static Intent createCustomSelectorIntent(@NonNull Context context, int type) { - storeType(context, type); - return new Intent(context, CustomSelectorActivity.class); - } - - private static Intent createCameraForImageIntent(@NonNull Context context, int type) { - storeType(context, type); - - Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); - try { - Uri capturedImageUri = createCameraPictureFile(context); - //We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20 - grantWritePermission(context, intent, capturedImageUri); - intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri); - } catch (Exception e) { - e.printStackTrace(); - } - - return intent; - } - - private static void revokeWritePermission(@NonNull Context context, Uri uri) { - context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - - private static void grantWritePermission(@NonNull Context context, Intent intent, Uri uri) { - List resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - - private static void storeType(@NonNull Context context, int type) { - PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply(); - } - - private static int restoreType(@NonNull Context context) { - return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0); - } - - /** - * Opens default galery or a available galleries picker if there is no default - * - * @param type Custom type of your choice, which will be returned with the images - */ - public static void openGallery(Activity activity, ActivityResultLauncher resultLauncher, int type, boolean openDocumentIntentPreferred) { - Intent intent = createGalleryIntent(activity, type, openDocumentIntentPreferred); - resultLauncher.launch(intent); - } - - /** - * Opens Custom Selector - */ - public static void openCustomSelector(Activity activity, ActivityResultLauncher resultLauncher, int type) { - Intent intent = createCustomSelectorIntent(activity, type); - resultLauncher.launch(intent); - } - - /** - * Opens the camera app to pick image clicked by user - */ - public static void openCameraForImage(Activity activity, ActivityResultLauncher resultLauncher, int type) { - Intent intent = createCameraForImageIntent(activity, type); - resultLauncher.launch(intent); - } - - @Nullable - private static UploadableFile takenCameraPicture(Context context) throws URISyntaxException { - String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_PHOTO, null); - if (lastCameraPhoto != null) { - return new UploadableFile(new File(lastCameraPhoto)); - } else { - return null; - } - } - - @Nullable - private static UploadableFile takenCameraVideo(Context context) throws URISyntaxException { - String lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context).getString(KEY_LAST_CAMERA_VIDEO, null); - if (lastCameraPhoto != null) { - return new UploadableFile(new File(lastCameraPhoto)); - } else { - return null; - } - } - - public static List handleExternalImagesPicked(Intent data, Activity activity) { - try { - return getFilesFromGalleryPictures(data, activity); - } catch (IOException | SecurityException e) { - e.printStackTrace(); - } - return new ArrayList<>(); - } - - private static boolean isPhoto(Intent data) { - return data == null || (data.getData() == null && data.getClipData() == null); - } - - private static Intent plainGalleryPickerIntent(boolean openDocumentIntentPreferred) { - /* - * Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue - * in the custom selector in Contributions fragment. - * Detailed discussion: https://github.com/commons-app/apps-android-commons/issues/5015 - * - * This permission check, however, was insufficient to fix location-loss in - * the regular selector in Contributions fragment and Nearby fragment, - * especially on some devices running Android 13 that use the new Photo Picker by default. - * - * New Photo Picker: https://developer.android.com/training/data-storage/shared/photopicker - * - * The new Photo Picker introduced by Android redacts location tags from EXIF metadata. - * Reported on the Google Issue Tracker: https://issuetracker.google.com/issues/243294058 - * Status: Won't fix (Intended behaviour) - * - * Switched intent from ACTION_GET_CONTENT to ACTION_OPEN_DOCUMENT (by default; can - * be changed through the Setting page) as: - * - * ACTION_GET_CONTENT opens the 'best application' for choosing that kind of data - * The best application is the new Photo Picker that redacts the location tags - * - * ACTION_OPEN_DOCUMENT, however, displays the various DocumentsProvider instances - * installed on the device, letting the user interactively navigate through them. - * - * So, this allows us to use the traditional file picker that does not redact location tags - * from EXIF. - * - */ - Intent intent; - if (openDocumentIntentPreferred) { - intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); - } else { - intent = new Intent(Intent.ACTION_GET_CONTENT); - } - intent.setType("image/*"); - return intent; - } - - public static void onPictureReturnedFromDocuments(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ - try { - Uri photoPath = result.getData().getData(); - UploadableFile photoFile = PickedFiles.pickedExistingPicture(activity, photoPath); - callbacks.onImagesPicked(singleFileList(photoFile), FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); - } - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - } - } else { - callbacks.onCanceled(FilePicker.ImageSource.DOCUMENTS, restoreType(activity)); - } - } - - /** - * onPictureReturnedFromCustomSelector. - * Retrieve and forward the images to upload wizard through callback. - */ - public static void onPictureReturnedFromCustomSelector(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(result.getResultCode() == Activity.RESULT_OK){ - try { - List files = getFilesFromCustomSelector(result.getData(), activity); - callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } - } else { - callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)); - } - } - - /** - * Get files from custom selector - * Retrieve and process the selected images from the custom selector. - */ - private static List getFilesFromCustomSelector(Intent data, Activity activity) throws IOException, SecurityException { - List files = new ArrayList<>(); - ArrayList images = data.getParcelableArrayListExtra("Images"); - for(Image image : images) { - Uri uri = image.getUri(); - UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); - files.add(file); - } - - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, files); - } - - return files; - } - - public static void onPictureReturnedFromGallery(ActivityResult result, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(result.getResultCode() == Activity.RESULT_OK && !isPhoto(result.getData())){ - try { - List files = getFilesFromGalleryPictures(result.getData(), activity); - callbacks.onImagesPicked(files, FilePicker.ImageSource.GALLERY, restoreType(activity)); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.GALLERY, restoreType(activity)); - } - } else{ - callbacks.onCanceled(FilePicker.ImageSource.GALLERY, restoreType(activity)); - } - } - - private static List getFilesFromGalleryPictures(Intent data, Activity activity) throws IOException, SecurityException { - List files = new ArrayList<>(); - ClipData clipData = data.getClipData(); - if (clipData == null) { - Uri uri = data.getData(); - UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); - files.add(file); - } else { - for (int i = 0; i < clipData.getItemCount(); i++) { - Uri uri = clipData.getItemAt(i).getUri(); - UploadableFile file = PickedFiles.pickedExistingPicture(activity, uri); - files.add(file); - } - } - - if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, files); - } - - return files; - } - - public static void onPictureReturnedFromCamera(ActivityResult activityResult, Activity activity, @NonNull FilePicker.Callbacks callbacks) { - if(activityResult.getResultCode() == Activity.RESULT_OK){ - try { - String lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity).getString(KEY_PHOTO_URI, null); - if (!TextUtils.isEmpty(lastImageUri)) { - revokeWritePermission(activity, Uri.parse(lastImageUri)); - } - - UploadableFile photoFile = FilePicker.takenCameraPicture(activity); - List files = new ArrayList<>(); - files.add(photoFile); - - if (photoFile == null) { - Exception e = new IllegalStateException("Unable to get the picture returned from camera"); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } else { - if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { - PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)); - } - - callbacks.onImagesPicked(files, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - - PreferenceManager.getDefaultSharedPreferences(activity) - .edit() - .remove(KEY_LAST_CAMERA_PHOTO) - .remove(KEY_PHOTO_URI) - .apply(); - } catch (Exception e) { - e.printStackTrace(); - callbacks.onImagePickerError(e, FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - } else { - callbacks.onCanceled(FilePicker.ImageSource.CAMERA_IMAGE, restoreType(activity)); - } - } - - public static FilePickerConfiguration configuration(@NonNull Context context) { - return new FilePickerConfiguration(context); - } - - - public enum ImageSource { - GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR - } - - public interface Callbacks { - void onImagePickerError(Exception e, FilePicker.ImageSource source, int type); - - void onImagesPicked(@NonNull List imageFiles, FilePicker.ImageSource source, int type); - - void onCanceled(FilePicker.ImageSource source, int type); - } - - public interface HandleActivityResult{ - void onHandleActivityResult(FilePicker.Callbacks callbacks); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt new file mode 100644 index 0000000000..6bf8a10613 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePicker.kt @@ -0,0 +1,441 @@ +package fr.free.nrw.commons.filepicker + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.MediaStore +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.preference.PreferenceManager +import fr.free.nrw.commons.customselector.model.Image +import fr.free.nrw.commons.customselector.ui.selector.CustomSelectorActivity +import fr.free.nrw.commons.filepicker.PickedFiles.singleFileList +import java.io.File +import java.io.IOException +import java.net.URISyntaxException + + +object FilePicker : Constants { + + private const val KEY_PHOTO_URI = "photo_uri" + private const val KEY_VIDEO_URI = "video_uri" + private const val KEY_LAST_CAMERA_PHOTO = "last_photo" + private const val KEY_LAST_CAMERA_VIDEO = "last_video" + private const val KEY_TYPE = "type" + + /** + * Returns the uri of the clicked image so that it can be put in MediaStore + */ + @Throws(IOException::class) + @JvmStatic + private fun createCameraPictureFile(context: Context): Uri { + val imagePath = PickedFiles.getCameraPicturesLocation(context) + val uri = PickedFiles.getUriToFile(context, imagePath) + val editor = PreferenceManager.getDefaultSharedPreferences(context).edit() + editor.putString(KEY_PHOTO_URI, uri.toString()) + editor.putString(KEY_LAST_CAMERA_PHOTO, imagePath.toString()) + editor.apply() + return uri + } + + + @JvmStatic + private fun createGalleryIntent( + context: Context, + type: Int, + openDocumentIntentPreferred: Boolean + ): Intent { + // storing picked image type to shared preferences + storeType(context, type) + // Supported types are SVG, PNG and JPEG, GIF, TIFF, WebP, XCF + val mimeTypes = arrayOf( + "image/jpg", + "image/png", + "image/jpeg", + "image/gif", + "image/tiff", + "image/webp", + "image/xcf", + "image/svg+xml", + "image/webp" + ) + return plainGalleryPickerIntent(openDocumentIntentPreferred) + .putExtra( + Intent.EXTRA_ALLOW_MULTIPLE, + configuration(context).allowsMultiplePickingInGallery() + ) + .putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + } + + /** + * CreateCustomSectorIntent, creates intent for custom selector activity. + * @param context + * @param type + * @return Custom selector intent + */ + @JvmStatic + private fun createCustomSelectorIntent(context: Context, type: Int): Intent { + storeType(context, type) + return Intent(context, CustomSelectorActivity::class.java) + } + + @JvmStatic + private fun createCameraForImageIntent(context: Context, type: Int): Intent { + storeType(context, type) + + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + try { + val capturedImageUri = createCameraPictureFile(context) + // We have to explicitly grant the write permission since Intent.setFlag works only on API Level >=20 + grantWritePermission(context, intent, capturedImageUri) + intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri) + } catch (e: Exception) { + e.printStackTrace() + } + + return intent + } + + @JvmStatic + private fun revokeWritePermission(context: Context, uri: Uri) { + context.revokeUriPermission( + uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + + @JvmStatic + private fun grantWritePermission(context: Context, intent: Intent, uri: Uri) { + val resInfoList = + context.packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + for (resolveInfo in resInfoList) { + val packageName = resolveInfo.activityInfo.packageName + context.grantUriPermission( + packageName, + uri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } + } + + @JvmStatic + private fun storeType(context: Context, type: Int) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putInt(KEY_TYPE, type).apply() + } + + @JvmStatic + private fun restoreType(context: Context): Int { + return PreferenceManager.getDefaultSharedPreferences(context).getInt(KEY_TYPE, 0) + } + + /** + * Opens default gallery or available galleries picker if there is no default + * + * @param type Custom type of your choice, which will be returned with the images + */ + @JvmStatic + fun openGallery( + activity: Activity, + resultLauncher: ActivityResultLauncher, + type: Int, + openDocumentIntentPreferred: Boolean + ) { + val intent = createGalleryIntent(activity, type, openDocumentIntentPreferred) + resultLauncher.launch(intent) + } + + /** + * Opens Custom Selector + */ + @JvmStatic + fun openCustomSelector( + activity: Activity, + resultLauncher: ActivityResultLauncher, + type: Int + ) { + val intent = createCustomSelectorIntent(activity, type) + resultLauncher.launch(intent) + } + + /** + * Opens the camera app to pick image clicked by user + */ + @JvmStatic + fun openCameraForImage( + activity: Activity, + resultLauncher: ActivityResultLauncher, + type: Int + ) { + val intent = createCameraForImageIntent(activity, type) + resultLauncher.launch(intent) + } + + @Throws(URISyntaxException::class) + @JvmStatic + private fun takenCameraPicture(context: Context): UploadableFile? { + val lastCameraPhoto = PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_LAST_CAMERA_PHOTO, null) + return if (lastCameraPhoto != null) { + UploadableFile(File(lastCameraPhoto)) + } else { + null + } + } + + @Throws(URISyntaxException::class) + @JvmStatic + private fun takenCameraVideo(context: Context): UploadableFile? { + val lastCameraVideo = PreferenceManager.getDefaultSharedPreferences(context) + .getString(KEY_LAST_CAMERA_VIDEO, null) + return if (lastCameraVideo != null) { + UploadableFile(File(lastCameraVideo)) + } else { + null + } + } + + @JvmStatic + fun handleExternalImagesPicked(data: Intent?, activity: Activity): List { + return try { + getFilesFromGalleryPictures(data, activity) + } catch (e: IOException) { + e.printStackTrace() + emptyList() + } catch (e: SecurityException) { + e.printStackTrace() + emptyList() + } + } + + @JvmStatic + private fun isPhoto(data: Intent?): Boolean { + return data == null || (data.data == null && data.clipData == null) + } + + @JvmStatic + private fun plainGalleryPickerIntent( + openDocumentIntentPreferred: Boolean + ): Intent { + /* + * Asking for ACCESS_MEDIA_LOCATION at runtime solved the location-loss issue + * in the custom selector in Contributions fragment. + * Detailed discussion: https://github.com/commons-app/apps-android-commons/issues/5015 + * + * This permission check, however, was insufficient to fix location-loss in + * the regular selector in Contributions fragment and Nearby fragment, + * especially on some devices running Android 13 that use the new Photo Picker by default. + * + * New Photo Picker: https://developer.android.com/training/data-storage/shared/photopicker + * + * The new Photo Picker introduced by Android redacts location tags from EXIF metadata. + * Reported on the Google Issue Tracker: https://issuetracker.google.com/issues/243294058 + * Status: Won't fix (Intended behaviour) + * + * Switched intent from ACTION_GET_CONTENT to ACTION_OPEN_DOCUMENT (by default; can + * be changed through the Setting page) as: + * + * ACTION_GET_CONTENT opens the 'best application' for choosing that kind of data + * The best application is the new Photo Picker that redacts the location tags + * + * ACTION_OPEN_DOCUMENT, however, displays the various DocumentsProvider instances + * installed on the device, letting the user interactively navigate through them. + * + * So, this allows us to use the traditional file picker that does not redact location tags + * from EXIF. + * + */ + val intent = if (openDocumentIntentPreferred) { + Intent(Intent.ACTION_OPEN_DOCUMENT) + } else { + Intent(Intent.ACTION_GET_CONTENT) + } + intent.type = "image/*" + return intent + } + + @JvmStatic + fun onPictureReturnedFromDocuments( + result: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) { + try { + val photoPath = result.data?.data + val photoFile = PickedFiles.pickedExistingPicture(activity, photoPath!!) + callbacks.onImagesPicked( + singleFileList(photoFile), + ImageSource.DOCUMENTS, + restoreType(activity) + ) + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)) + } + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.DOCUMENTS, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.DOCUMENTS, restoreType(activity)) + } + } + + /** + * onPictureReturnedFromCustomSelector. + * Retrieve and forward the images to upload wizard through callback. + */ + @JvmStatic + fun onPictureReturnedFromCustomSelector( + result: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (result.resultCode == Activity.RESULT_OK) { + try { + val files = getFilesFromCustomSelector(result.data, activity) + callbacks.onImagesPicked(files, ImageSource.CUSTOM_SELECTOR, restoreType(activity)) + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.CUSTOM_SELECTOR, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.CUSTOM_SELECTOR, restoreType(activity)) + } + } + + /** + * Get files from custom selector + * Retrieve and process the selected images from the custom selector. + */ + @Throws(IOException::class, SecurityException::class) + @JvmStatic + private fun getFilesFromCustomSelector( + data: Intent?, + activity: Activity + ): List { + val files = mutableListOf() + val images = data?.getParcelableArrayListExtra("Images") + images?.forEach { image -> + val uri = image.uri + val file = PickedFiles.pickedExistingPicture(activity, uri) + files.add(file) + } + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, files) + } + + return files + } + + @JvmStatic + fun onPictureReturnedFromGallery( + result: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) { + try { + val files = getFilesFromGalleryPictures(result.data, activity) + callbacks.onImagesPicked(files, ImageSource.GALLERY, restoreType(activity)) + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.GALLERY, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.GALLERY, restoreType(activity)) + } + } + + @Throws(IOException::class, SecurityException::class) + @JvmStatic + private fun getFilesFromGalleryPictures( + data: Intent?, + activity: Activity + ): List { + val files = mutableListOf() + val clipData = data?.clipData + if (clipData == null) { + val uri = data?.data + val file = PickedFiles.pickedExistingPicture(activity, uri!!) + files.add(file) + } else { + for (i in 0 until clipData.itemCount) { + val uri = clipData.getItemAt(i).uri + val file = PickedFiles.pickedExistingPicture(activity, uri) + files.add(file) + } + } + + if (configuration(activity).shouldCopyPickedImagesToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, files) + } + + return files + } + + @JvmStatic + fun onPictureReturnedFromCamera( + activityResult: ActivityResult, + activity: Activity, + callbacks: Callbacks + ) { + if (activityResult.resultCode == Activity.RESULT_OK) { + try { + val lastImageUri = PreferenceManager.getDefaultSharedPreferences(activity) + .getString(KEY_PHOTO_URI, null) + if (!lastImageUri.isNullOrEmpty()) { + revokeWritePermission(activity, Uri.parse(lastImageUri)) + } + + val photoFile = takenCameraPicture(activity) + val files = mutableListOf() + photoFile?.let { files.add(it) } + + if (photoFile == null) { + val e = IllegalStateException("Unable to get the picture returned from camera") + callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity)) + } else { + if (configuration(activity).shouldCopyTakenPhotosToPublicGalleryAppFolder()) { + PickedFiles.copyFilesInSeparateThread(activity, singleFileList(photoFile)) + } + callbacks.onImagesPicked(files, ImageSource.CAMERA_IMAGE, restoreType(activity)) + } + + PreferenceManager.getDefaultSharedPreferences(activity).edit() + .remove(KEY_LAST_CAMERA_PHOTO) + .remove(KEY_PHOTO_URI) + .apply() + } catch (e: Exception) { + e.printStackTrace() + callbacks.onImagePickerError(e, ImageSource.CAMERA_IMAGE, restoreType(activity)) + } + } else { + callbacks.onCanceled(ImageSource.CAMERA_IMAGE, restoreType(activity)) + } + } + + @JvmStatic + fun configuration(context: Context): FilePickerConfiguration { + return FilePickerConfiguration(context) + } + + enum class ImageSource { + GALLERY, DOCUMENTS, CAMERA_IMAGE, CAMERA_VIDEO, CUSTOM_SELECTOR + } + + interface Callbacks { + fun onImagePickerError(e: Exception, source: ImageSource, type: Int) + + fun onImagesPicked(imageFiles: List, source: ImageSource, type: Int) + + fun onCanceled(source: ImageSource, type: Int) + } + + interface HandleActivityResult { + fun onHandleActivityResult(callbacks: Callbacks) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java deleted file mode 100644 index 08a204e8b9..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.java +++ /dev/null @@ -1,44 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.content.Context; -import androidx.preference.PreferenceManager; - -public class FilePickerConfiguration implements Constants { - - private Context context; - - FilePickerConfiguration(Context context) { - this.context = context; - } - - public FilePickerConfiguration setAllowMultiplePickInGallery(boolean allowMultiple) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putBoolean(BundleKeys.ALLOW_MULTIPLE, allowMultiple) - .apply(); - return this; - } - - public FilePickerConfiguration setCopyTakenPhotosToPublicGalleryAppFolder(boolean copy) { - PreferenceManager.getDefaultSharedPreferences(context).edit() - .putBoolean(BundleKeys.COPY_TAKEN_PHOTOS, copy) - .apply(); - return this; - } - - public String getFolderName() { - return PreferenceManager.getDefaultSharedPreferences(context).getString(BundleKeys.FOLDER_NAME, DEFAULT_FOLDER_NAME); - } - - public boolean allowsMultiplePickingInGallery() { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.ALLOW_MULTIPLE, false); - } - - public boolean shouldCopyTakenPhotosToPublicGalleryAppFolder() { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_TAKEN_PHOTOS, false); - } - - public boolean shouldCopyPickedImagesToPublicGalleryAppFolder() { - return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(BundleKeys.COPY_PICKED_IMAGES, false); - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt new file mode 100644 index 0000000000..db025a5442 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/FilePickerConfiguration.kt @@ -0,0 +1,46 @@ +package fr.free.nrw.commons.filepicker + +import android.content.Context +import androidx.preference.PreferenceManager + +class FilePickerConfiguration( + private val context: Context +): Constants { + + fun setAllowMultiplePickInGallery(allowMultiple: Boolean): FilePickerConfiguration { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, allowMultiple) + .apply() + return this + } + + fun setCopyTakenPhotosToPublicGalleryAppFolder(copy: Boolean): FilePickerConfiguration { + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, copy) + .apply() + return this + } + + fun getFolderName(): String { + return PreferenceManager.getDefaultSharedPreferences(context) + .getString( + Constants.BundleKeys.FOLDER_NAME, + Constants.DEFAULT_FOLDER_NAME + ) ?: Constants.DEFAULT_FOLDER_NAME + } + + fun allowsMultiplePickingInGallery(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(Constants.BundleKeys.ALLOW_MULTIPLE, false) + } + + fun shouldCopyTakenPhotosToPublicGalleryAppFolder(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(Constants.BundleKeys.COPY_TAKEN_PHOTOS, false) + } + + fun shouldCopyPickedImagesToPublicGalleryAppFolder(): Boolean { + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean(Constants.BundleKeys.COPY_PICKED_IMAGES, false) + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java deleted file mode 100644 index e6c82f5c1c..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.java +++ /dev/null @@ -1,26 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.webkit.MimeTypeMap; - -import com.facebook.common.internal.ImmutableMap; - -import java.util.Map; - -public class MimeTypeMapWrapper { - - private static final MimeTypeMap sMimeTypeMap = MimeTypeMap.getSingleton(); - - private static final Map sMimeTypeToExtensionMap = - ImmutableMap.of( - "image/heif", "heif", - "image/heic", "heic"); - - public static String getExtensionFromMimeType(String mimeType) { - String result = sMimeTypeToExtensionMap.get(mimeType); - if (result != null) { - return result; - } - return sMimeTypeMap.getExtensionFromMimeType(mimeType); - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt new file mode 100644 index 0000000000..0cf21cc027 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/MimeTypeMapWrapper.kt @@ -0,0 +1,24 @@ +package fr.free.nrw.commons.filepicker + +import android.webkit.MimeTypeMap + +class MimeTypeMapWrapper { + + companion object { + private val sMimeTypeMap = MimeTypeMap.getSingleton() + + private val sMimeTypeToExtensionMap = mapOf( + "image/heif" to "heif", + "image/heic" to "heic" + ) + + @JvmStatic + fun getExtensionFromMimeType(mimeType: String): String? { + val result = sMimeTypeToExtensionMap[mimeType] + if (result != null) { + return result + } + return sMimeTypeMap.getExtensionFromMimeType(mimeType) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java deleted file mode 100644 index ca1abba623..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.java +++ /dev/null @@ -1,208 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.content.ContentResolver; -import android.content.Context; -import android.media.MediaScannerConnection; -import android.net.Uri; -import android.os.Environment; -import android.webkit.MimeTypeMap; - -import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.List; -import java.util.UUID; - -import timber.log.Timber; - -/** - * PickedFiles. - * Process the upload items. - */ -public class PickedFiles implements Constants { - - /** - * Get Folder Name - * @param context - * @return default application folder name. - */ - private static String getFolderName(@NonNull Context context) { - return FilePicker.configuration(context).getFolderName(); - } - - /** - * tempImageDirectory - * @param context - * @return temporary image directory to copy and perform exif changes. - */ - private static File tempImageDirectory(@NonNull Context context) { - File privateTempDir = new File(context.getCacheDir(), DEFAULT_FOLDER_NAME); - if (!privateTempDir.exists()) privateTempDir.mkdirs(); - return privateTempDir; - } - - /** - * writeToFile - * writes inputStream data to the destination file. - * @param in input stream of source file. - * @param file destination file - */ - private static void writeToFile(InputStream in, File file) throws IOException { - try (OutputStream out = new FileOutputStream(file)) { - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } - } - - /** - * Copy file function. - * Copies source file to destination file. - * @param src source file - * @param dst destination file - * @throws IOException (File input stream exception) - */ - private static void copyFile(File src, File dst) throws IOException { - try (InputStream in = new FileInputStream(src)) { - writeToFile(in, dst); - } - } - - /** - * Copy files in separate thread. - * Copies all the uploadable files to the temp image folder on background thread. - * @param context - * @param filesToCopy uploadable file list to be copied. - */ - static void copyFilesInSeparateThread(final Context context, final List filesToCopy) { - new Thread(() -> { - List copiedFiles = new ArrayList<>(); - int i = 1; - for (UploadableFile uploadableFile : filesToCopy) { - File fileToCopy = uploadableFile.getFile(); - File dstDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), getFolderName(context)); - if (!dstDir.exists()) { - dstDir.mkdirs(); - } - - String[] filenameSplit = fileToCopy.getName().split("\\."); - String extension = "." + filenameSplit[filenameSplit.length - 1]; - String filename = String.format("IMG_%s_%d.%s", new SimpleDateFormat("yyyyMMdd_HHmmss").format(Calendar.getInstance().getTime()), i, extension); - - File dstFile = new File(dstDir, filename); - try { - dstFile.createNewFile(); - copyFile(fileToCopy, dstFile); - copiedFiles.add(dstFile); - } catch (IOException e) { - e.printStackTrace(); - } - i++; - } - scanCopiedImages(context, copiedFiles); - }).run(); - } - - /** - * singleFileList. - * converts a single uploadableFile to list of uploadableFile. - * @param file uploadable file - * @return - */ - static List singleFileList(UploadableFile file) { - List list = new ArrayList<>(); - list.add(file); - return list; - } - - /** - * ScanCopiedImages - * Scan copied images metadata using media scanner. - * @param context - * @param copiedImages copied images list. - */ - static void scanCopiedImages(Context context, List copiedImages) { - String[] paths = new String[copiedImages.size()]; - for (int i = 0; i < copiedImages.size(); i++) { - paths[i] = copiedImages.get(i).toString(); - } - - MediaScannerConnection.scanFile(context, - paths, null, - (path, uri) -> { - Timber.d("Scanned " + path + ":"); - Timber.d("-> uri=%s", uri); - }); - } - - /** - * pickedExistingPicture - * convert the image into uploadable file. - * @param photoUri Uri of the image. - * @return Uploadable file ready for tag redaction. - */ - public static UploadableFile pickedExistingPicture(@NonNull Context context, Uri photoUri) throws IOException, SecurityException {// SecurityException for those file providers who share URI but forget to grant necessary permissions - File directory = tempImageDirectory(context); - File photoFile = new File(directory, UUID.randomUUID().toString() + "." + getMimeType(context, photoUri)); - if (photoFile.createNewFile()) { - try (InputStream pictureInputStream = context.getContentResolver().openInputStream(photoUri)) { - writeToFile(pictureInputStream, photoFile); - } - } else { - throw new IOException("could not create photoFile to write upon"); - } - return new UploadableFile(photoUri, photoFile); - } - - /** - * getCameraPictureLocation - */ - static File getCameraPicturesLocation(@NonNull Context context) throws IOException { - File dir = tempImageDirectory(context); - return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir); - } - - /** - * To find out the extension of required object in given uri - * Solution by http://stackoverflow.com/a/36514823/1171484 - */ - private static String getMimeType(@NonNull Context context, @NonNull Uri uri) { - String extension; - - //Check uri format to avoid null - if (uri.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { - //If scheme is a content - extension = MimeTypeMapWrapper.getExtensionFromMimeType(context.getContentResolver().getType(uri)); - } else { - //If scheme is a File - //This will replace white spaces with %20 and also other special characters. This will avoid returning null values on file name with spaces and special characters. - extension = MimeTypeMap.getFileExtensionFromUrl(Uri.fromFile(new File(uri.getPath())).toString()); - - } - - return extension; - } - - /** - * GetUriToFile - * @param file get uri of file - * @return uri of requested file. - */ - static Uri getUriToFile(@NonNull Context context, @NonNull File file) { - String packageName = context.getApplicationContext().getPackageName(); - String authority = packageName + ".provider"; - return FileProvider.getUriForFile(context, authority, file); - } - -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt new file mode 100644 index 0000000000..9694dedb53 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/PickedFiles.kt @@ -0,0 +1,195 @@ +package fr.free.nrw.commons.filepicker + +import android.content.ContentResolver +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Environment +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import fr.free.nrw.commons.filepicker.Constants.Companion.DEFAULT_FOLDER_NAME +import timber.log.Timber +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException +import java.io.InputStream +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.UUID + + +/** + * PickedFiles. + * Process the upload items. + */ +object PickedFiles : Constants { + + /** + * Get Folder Name + * @return default application folder name. + */ + @JvmStatic + private fun getFolderName(context: Context): String { + return FilePicker.configuration(context).getFolderName() + } + + /** + * tempImageDirectory + * @return temporary image directory to copy and perform exif changes. + */ + @JvmStatic + private fun tempImageDirectory(context: Context): File { + val privateTempDir = File(context.cacheDir, DEFAULT_FOLDER_NAME) + if (!privateTempDir.exists()) privateTempDir.mkdirs() + return privateTempDir + } + + /** + * writeToFile + * Writes inputStream data to the destination file. + */ + @JvmStatic + @Throws(IOException::class) + private fun writeToFile(inputStream: InputStream, file: File) { + inputStream.use { input -> + FileOutputStream(file).use { output -> + val buffer = ByteArray(1024) + var length: Int + while (input.read(buffer).also { length = it } > 0) { + output.write(buffer, 0, length) + } + } + } + } + + /** + * Copy file function. + * Copies source file to destination file. + */ + @Throws(IOException::class) + @JvmStatic + private fun copyFile(src: File, dst: File) { + FileInputStream(src).use { inputStream -> + writeToFile(inputStream, dst) + } + } + + /** + * Copy files in separate thread. + * Copies all the uploadable files to the temp image folder on background thread. + */ + @JvmStatic + fun copyFilesInSeparateThread(context: Context, filesToCopy: List) { + Thread { + val copiedFiles = mutableListOf() + var index = 1 + filesToCopy.forEach { uploadableFile -> + val fileToCopy = uploadableFile.file + val dstDir = File( + Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), + getFolderName(context) + ) + if (!dstDir.exists()) dstDir.mkdirs() + + val filenameSplit = fileToCopy.name.split(".") + val extension = ".${filenameSplit.last()}" + val filename = "IMG_${SimpleDateFormat( + "yyyyMMdd_HHmmss", + Locale.getDefault()).format(Date())}_$index$extension" + val dstFile = File(dstDir, filename) + + try { + dstFile.createNewFile() + copyFile(fileToCopy, dstFile) + copiedFiles.add(dstFile) + } catch (e: IOException) { + e.printStackTrace() + } + index++ + } + scanCopiedImages(context, copiedFiles) + }.start() + } + + /** + * singleFileList + * Converts a single uploadableFile to list of uploadableFile. + */ + @JvmStatic + fun singleFileList(file: UploadableFile): List { + return listOf(file) + } + + /** + * ScanCopiedImages + * Scans copied images metadata using media scanner. + */ + @JvmStatic + fun scanCopiedImages(context: Context, copiedImages: List) { + val paths = copiedImages.map { it.toString() }.toTypedArray() + MediaScannerConnection.scanFile(context, paths, null) { path, uri -> + Timber.d("Scanned $path:") + Timber.d("-> uri=$uri") + } + } + + /** + * pickedExistingPicture + * Convert the image into uploadable file. + */ + @Throws(IOException::class, SecurityException::class) + @JvmStatic + fun pickedExistingPicture(context: Context, photoUri: Uri): UploadableFile { + val directory = tempImageDirectory(context) + val mimeType = getMimeType(context, photoUri) + val photoFile = File(directory, "${UUID.randomUUID()}.$mimeType") + + if (photoFile.createNewFile()) { + context.contentResolver.openInputStream(photoUri)?.use { inputStream -> + writeToFile(inputStream, photoFile) + } + } else { + throw IOException("Could not create photoFile to write upon") + } + return UploadableFile(photoUri, photoFile) + } + + /** + * getCameraPictureLocation + */ + @Throws(IOException::class) + @JvmStatic + fun getCameraPicturesLocation(context: Context): File { + val dir = tempImageDirectory(context) + return File.createTempFile(UUID.randomUUID().toString(), ".jpg", dir) + } + + /** + * To find out the extension of the required object in a given uri + */ + @JvmStatic + private fun getMimeType(context: Context, uri: Uri): String { + return if (uri.scheme == ContentResolver.SCHEME_CONTENT) { + context.contentResolver.getType(uri) + ?.let { MimeTypeMapWrapper.getExtensionFromMimeType(it) } + } else { + MimeTypeMap.getFileExtensionFromUrl( + Uri.fromFile(uri.path?.let { File(it) }).toString() + ) + } ?: "jpg" // Default to jpg if unable to determine type + } + + /** + * GetUriToFile + * @param file get uri of file + * @return uri of requested file. + */ + @JvmStatic + fun getUriToFile(context: Context, file: File): Uri { + val packageName = context.applicationContext.packageName + val authority = "$packageName.provider" + return FileProvider.getUriForFile(context, authority, file) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java deleted file mode 100644 index 1fe306a8b5..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.java +++ /dev/null @@ -1,213 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.Nullable; -import androidx.exifinterface.media.ExifInterface; - -import fr.free.nrw.commons.upload.FileUtils; -import java.io.File; -import java.io.IOException; -import java.util.Date; -import timber.log.Timber; - -public class UploadableFile implements Parcelable { - public static final Creator CREATOR = new Creator() { - @Override - public UploadableFile createFromParcel(Parcel in) { - return new UploadableFile(in); - } - - @Override - public UploadableFile[] newArray(int size) { - return new UploadableFile[size]; - } - }; - - private final Uri contentUri; - private final File file; - - public UploadableFile(Uri contentUri, File file) { - this.contentUri = contentUri; - this.file = file; - } - - public UploadableFile(File file) { - this.file = file; - this.contentUri = Uri.fromFile(new File(file.getPath())); - } - - public UploadableFile(Parcel in) { - this.contentUri = in.readParcelable(Uri.class.getClassLoader()); - file = (File) in.readSerializable(); - } - - public Uri getContentUri() { - return contentUri; - } - - public File getFile() { - return file; - } - - public String getFilePath() { - return file.getPath(); - } - - public Uri getMediaUri() { - return Uri.parse(getFilePath()); - } - - public String getMimeType(Context context) { - return FileUtils.getMimeType(context, getMediaUri()); - } - - @Override - public int describeContents() { - return 0; - } - - /** - * First try to get the file creation date from EXIF else fall back to CP - * @param context - * @return - */ - @Nullable - public DateTimeWithSource getFileCreatedDate(Context context) { - DateTimeWithSource dateTimeFromExif = getDateTimeFromExif(); - if (dateTimeFromExif == null) { - return getFileCreatedDateFromCP(context); - } else { - return dateTimeFromExif; - } - } - - /** - * Get filePath creation date from uri from all possible content providers - * - * @return - */ - private DateTimeWithSource getFileCreatedDateFromCP(Context context) { - try { - Cursor cursor = context.getContentResolver().query(contentUri, null, null, null, null); - if (cursor == null) { - return null;//Could not fetch last_modified - } - //Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases - int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app - if (lastModifiedColumnIndex == -1) { - lastModifiedColumnIndex = cursor.getColumnIndex("datetaken"); - } - //If both the content providers do not give the data, lets leave it to Jesus - if (lastModifiedColumnIndex == -1) { - cursor.close(); - return null; - } - cursor.moveToFirst(); - return new DateTimeWithSource(cursor.getLong(lastModifiedColumnIndex), DateTimeWithSource.CP_SOURCE); - } catch (Exception e) { - return null;////Could not fetch last_modified - } - } - - /** - * Indicate whether the EXIF contains the location (both latitude and longitude). - * - * @return whether the location exists for the file's EXIF - */ - public boolean hasLocation() { - try { - ExifInterface exif = new ExifInterface(file.getAbsolutePath()); - final String latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - final String longitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE); - return latitude != null && longitude != null; - } catch (IOException | NumberFormatException | IndexOutOfBoundsException e) { - Timber.tag("UploadableFile"); - Timber.d(e); - } - return false; - } - - /** - * Get filePath creation date from uri from EXIF - * - * @return - */ - private DateTimeWithSource getDateTimeFromExif() { - try { - ExifInterface exif = new ExifInterface(file.getAbsolutePath()); - // TAG_DATETIME returns the last edited date, we need TAG_DATETIME_ORIGINAL for creation date - // See issue https://github.com/commons-app/apps-android-commons/issues/1971 - String dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL); - if (dateTimeSubString!=null) { //getAttribute may return null - String year = dateTimeSubString.substring(0,4); - String month = dateTimeSubString.substring(5,7); - String day = dateTimeSubString.substring(8,10); - // This date is stored as a string (not as a date), the rason is we don't want to include timezones - String dateCreatedString = String.format("%04d-%02d-%02d", Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day)); - if (dateCreatedString.length() == 10) { //yyyy-MM-dd format of date is expected - @SuppressLint("RestrictedApi") Long dateTime = exif.getDateTimeOriginal(); - if(dateTime != null){ - Date date = new Date(dateTime); - return new DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE); - } - } - } - } catch (IOException | NumberFormatException | IndexOutOfBoundsException e) { - Timber.tag("UploadableFile"); - Timber.d(e); - } - return null; - } - - @Override - public void writeToParcel(Parcel parcel, int i) { - parcel.writeParcelable(contentUri, 0); - parcel.writeSerializable(file); - } - - /** - * This class contains the epochDate along with the source from which it was extracted - */ - public class DateTimeWithSource { - public static final String CP_SOURCE = "contentProvider"; - public static final String EXIF_SOURCE = "exif"; - - private final long epochDate; - private String dateString; // this does not includes timezone information - private final String source; - - public DateTimeWithSource(long epochDate, String source) { - this.epochDate = epochDate; - this.source = source; - } - - public DateTimeWithSource(Date date, String source) { - this.epochDate = date.getTime(); - this.source = source; - } - - public DateTimeWithSource(Date date, String dateString, String source) { - this.epochDate = date.getTime(); - this.dateString = dateString; - this.source = source; - } - - public long getEpochDate() { - return epochDate; - } - - public String getDateString() { - return dateString; - } - - public String getSource() { - return source; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt new file mode 100644 index 0000000000..1398e77853 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/UploadableFile.kt @@ -0,0 +1,168 @@ +package fr.free.nrw.commons.filepicker + +import android.annotation.SuppressLint +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable + +import androidx.exifinterface.media.ExifInterface + +import fr.free.nrw.commons.upload.FileUtils +import java.io.File +import java.io.IOException +import java.util.Date +import timber.log.Timber + +class UploadableFile : Parcelable { + + val contentUri: Uri + val file: File + + constructor(contentUri: Uri, file: File) { + this.contentUri = contentUri + this.file = file + } + + constructor(file: File) { + this.file = file + this.contentUri = Uri.fromFile(File(file.path)) + } + + private constructor(parcel: Parcel) { + contentUri = parcel.readParcelable(Uri::class.java.classLoader)!! + file = parcel.readSerializable() as File + } + + fun getFilePath(): String { + return file.path + } + + fun getMediaUri(): Uri { + return Uri.parse(getFilePath()) + } + + fun getMimeType(context: Context): String? { + return FileUtils.getMimeType(context, getMediaUri()) + } + + override fun describeContents(): Int = 0 + + /** + * First try to get the file creation date from EXIF, else fall back to Content Provider (CP) + */ + fun getFileCreatedDate(context: Context): DateTimeWithSource? { + return getDateTimeFromExif() ?: getFileCreatedDateFromCP(context) + } + + /** + * Get filePath creation date from URI using all possible content providers + */ + private fun getFileCreatedDateFromCP(context: Context): DateTimeWithSource? { + return try { + val cursor: Cursor? = context.contentResolver.query(contentUri, null, null, null, null) + cursor?.use { + val lastModifiedColumnIndex = cursor + .getColumnIndex( + "last_modified" + ).takeIf { it != -1 } + ?: cursor.getColumnIndex("datetaken") + if (lastModifiedColumnIndex == -1) return null // No valid column found + cursor.moveToFirst() + DateTimeWithSource( + cursor.getLong( + lastModifiedColumnIndex + ), DateTimeWithSource.CP_SOURCE) + } + } catch (e: Exception) { + Timber.tag("UploadableFile").d(e) + null + } + } + + /** + * Indicates whether the EXIF contains the location (both latitude and longitude). + */ + fun hasLocation(): Boolean { + return try { + val exif = ExifInterface(file.absolutePath) + val latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) + val longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE) + latitude != null && longitude != null + } catch (e: IOException) { + Timber.tag("UploadableFile").d(e) + false + } + } + + /** + * Get filePath creation date from URI using EXIF data + */ + private fun getDateTimeFromExif(): DateTimeWithSource? { + return try { + val exif = ExifInterface(file.absolutePath) + val dateTimeSubString = exif.getAttribute(ExifInterface.TAG_DATETIME_ORIGINAL) + if (dateTimeSubString != null) { + val year = dateTimeSubString.substring(0, 4).toInt() + val month = dateTimeSubString.substring(5, 7).toInt() + val day = dateTimeSubString.substring(8, 10).toInt() + val dateCreatedString = "%04d-%02d-%02d".format(year, month, day) + if (dateCreatedString.length == 10) { + @SuppressLint("RestrictedApi") + val dateTime = exif.dateTimeOriginal + if (dateTime != null) { + val date = Date(dateTime) + return DateTimeWithSource(date, dateCreatedString, DateTimeWithSource.EXIF_SOURCE) + } + } + } + null + } catch (e: Exception) { + Timber.tag("UploadableFile").d(e) + null + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(contentUri, flags) + parcel.writeSerializable(file) + } + + class DateTimeWithSource { + companion object { + const val CP_SOURCE = "contentProvider" + const val EXIF_SOURCE = "exif" + } + + val epochDate: Long + var dateString: String? = null + val source: String + + constructor(epochDate: Long, source: String) { + this.epochDate = epochDate + this.source = source + } + + constructor(date: Date, source: String) { + epochDate = date.time + this.source = source + } + + constructor(date: Date, dateString: String, source: String) { + epochDate = date.time + this.dateString = dateString + this.source = source + } + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): UploadableFile { + return UploadableFile(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index 86ee5c4feb..91146059d4 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -38,6 +38,7 @@ import fr.free.nrw.commons.campaigns.CampaignView import fr.free.nrw.commons.contributions.ContributionController import fr.free.nrw.commons.contributions.MainActivity import fr.free.nrw.commons.di.ApplicationlessInjection +import fr.free.nrw.commons.filepicker.FilePicker import fr.free.nrw.commons.kvstore.JsonKvStore import fr.free.nrw.commons.location.LocationServiceManager import fr.free.nrw.commons.logging.CommonsLogSender @@ -83,9 +84,17 @@ class SettingsFragment : PreferenceFragmentCompat() { private val cameraPickLauncherForResult: ActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { result -> - contributionController.handleActivityResultWithCallback(requireActivity()) { callbacks -> - contributionController.onPictureReturnedFromCamera(result, requireActivity(), callbacks) - } + contributionController.handleActivityResultWithCallback( + requireActivity(), + object: FilePicker.HandleActivityResult { + override fun onHandleActivityResult(callbacks: FilePicker.Callbacks) { + contributionController.onPictureReturnedFromCamera( + result, + requireActivity(), + callbacks + ) + } + }) } /** diff --git a/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt b/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt index fc80252fc9..62bd3f1a90 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt +++ b/app/src/main/java/fr/free/nrw/commons/utils/CustomSelectorUtils.kt @@ -63,7 +63,7 @@ class CustomSelectorUtils { fileProcessor.redactExifTags(exifInterface, fileProcessor.getExifTagsToRedact()) val sha1 = fileUtilsWrapper.getSHA1( - fileUtilsWrapper.getFileInputStream(uploadableFile.filePath), + fileUtilsWrapper.getFileInputStream(uploadableFile.getFilePath()), ) uploadableFile.file.delete() sha1 diff --git a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java deleted file mode 100644 index 4da9e26906..0000000000 --- a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.java +++ /dev/null @@ -1,32 +0,0 @@ -package fr.free.nrw.commons.filepicker; - -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.provider.OpenableColumns; -import androidx.core.content.FileProvider; -import org.robolectric.annotation.Implementation; -import org.robolectric.annotation.Implements; - -@Implements(FileProvider.class) -public class ShadowFileProvider { - - @Implementation - public Cursor query(final Uri uri, final String[] projection, final String selection, - final String[] selectionArgs, - final String sortOrder) { - - if (uri == null) { - return null; - } - - final String[] columns = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}; - final Object[] values = {"dummy", 500}; - final MatrixCursor cursor = new MatrixCursor(columns, 1); - - if (!uri.equals(Uri.EMPTY)) { - cursor.addRow(values); - } - return cursor; - } -} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt new file mode 100644 index 0000000000..fc9d20cf64 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/filepicker/ShadowFileProvider.kt @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.filepicker + +import android.database.Cursor +import android.database.MatrixCursor +import android.net.Uri +import android.provider.OpenableColumns +import androidx.core.content.FileProvider +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements + +@Implements(FileProvider::class) +class ShadowFileProvider { + + @Implementation + fun query( + uri: Uri?, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + + if (uri == null) { + return null + } + + val columns = arrayOf(OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE) + val values = arrayOf("dummy", 500) + val cursor = MatrixCursor(columns, 1) + + if (uri != Uri.EMPTY) { + cursor.addRow(values) + } + return cursor + } +} diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt index 29a35c1e55..861d1a6a49 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/UploadPresenterTest.kt @@ -62,7 +62,7 @@ class UploadPresenterTest { `when`(repository.buildContributions()).thenReturn(Observable.just(contribution)) uploadableFiles.add(uploadableFile) `when`(view.uploadableFiles).thenReturn(uploadableFiles) - `when`(uploadableFile.filePath).thenReturn("data://test") + `when`(uploadableFile.getFilePath()).thenReturn("data://test") } /** From ae52267a277976474eb2fa7aa2826f3873765800 Mon Sep 17 00:00:00 2001 From: Paul Hawke Date: Fri, 6 Dec 2024 02:50:29 -0600 Subject: [PATCH 051/231] Convert wikidata/mwapi to kotlin (part 2) (#5999) * Convert DepictSearchResponse to kotlin * Convert Entities to kotlin * Convert WikiSite to kotlin --------- Co-authored-by: Nicolas Raoul --- .../fr/free/nrw/commons/AboutActivityTest.kt | 2 +- .../free/nrw/commons/di/NetworkingModule.kt | 6 +- .../wikidata/model/DepictSearchResponse.java | 24 -- .../wikidata/model/DepictSearchResponse.kt | 12 + .../nrw/commons/wikidata/model/Entities.java | 106 ------- .../nrw/commons/wikidata/model/Entities.kt | 64 ++++ .../nrw/commons/wikidata/model/WikiSite.java | 292 ------------------ .../nrw/commons/wikidata/model/WikiSite.kt | 269 ++++++++++++++++ 8 files changed, 348 insertions(+), 427 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt delete mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java create mode 100644 app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt diff --git a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt index 45ff9e49dd..50dfe8e7f0 100644 --- a/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt +++ b/app/src/androidTest/java/fr/free/nrw/commons/AboutActivityTest.kt @@ -105,7 +105,7 @@ class AboutActivityTest { fun testLaunchTranslate() { Espresso.onView(ViewMatchers.withId(R.id.about_translate)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(android.R.id.button1)).perform(ViewActions.click()) - val langCode = CommonsApplication.instance.languageLookUpTable!!.codes[0] + val langCode = CommonsApplication.instance.languageLookUpTable!!.getCodes()[0] Intents.intended( CoreMatchers.allOf( IntentMatchers.hasAction(Intent.ACTION_VIEW), diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt index 7ca3b4fd03..0e9d834784 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.kt @@ -44,7 +44,6 @@ import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor.Level import timber.log.Timber import java.io.File -import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Named import javax.inject.Singleton @@ -293,9 +292,8 @@ class NetworkingModule { @Provides @Singleton @Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE) - fun provideLanguageWikipediaSite(): WikiSite { - return WikiSite.forLanguageCode(Locale.getDefault().language) - } + fun provideLanguageWikipediaSite(): WikiSite = + WikiSite.forDefaultLocaleLanguageCode() companion object { private const val WIKIDATA_SPARQL_QUERY_URL = "https://query.wikidata.org/sparql" diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java deleted file mode 100644 index 8ea2fa1eda..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.java +++ /dev/null @@ -1,24 +0,0 @@ -package fr.free.nrw.commons.wikidata.model; - -import java.util.List; - -/** - * Model class for API response obtained from search for depictions - */ -public class DepictSearchResponse { - private final List search; - - /** - * Constructor to initialise value of the search object - */ - public DepictSearchResponse(List search) { - this.search = search; - } - - /** - * @return List for the DepictSearchResponse - */ - public List getSearch() { - return search; - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt new file mode 100644 index 0000000000..5a0ed8c49f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/DepictSearchResponse.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.wikidata.model + +/** + * Model class for API response obtained from search for depictions + */ +class DepictSearchResponse( + /** + * @return List for the DepictSearchResponse + + */ + val search: List +) diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java deleted file mode 100644 index 9dab836cf8..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.java +++ /dev/null @@ -1,106 +0,0 @@ -package fr.free.nrw.commons.wikidata.model; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.google.gson.annotations.SerializedName; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import org.apache.commons.lang3.StringUtils; -import org.jetbrains.annotations.NotNull; -import fr.free.nrw.commons.wikidata.mwapi.MwResponse; - - -public class Entities extends MwResponse { - @Nullable private Map entities; - private int success; - - @NotNull - public Map entities() { - return entities != null ? entities : Collections.emptyMap(); - } - - public int getSuccess() { - return success; - } - - @Nullable public Entity getFirst() { - if (entities == null) { - return null; - } - return entities.values().iterator().next(); - } - - @Override - public void postProcess() { - if (getFirst() != null && getFirst().isMissing()) { - throw new RuntimeException("The requested entity was not found."); - } - } - - public static class Entity { - @Nullable private String type; - @Nullable private String id; - @Nullable private Map labels; - @Nullable private Map descriptions; - @Nullable private Map sitelinks; - @Nullable @SerializedName(value = "statements", alternate = "claims") private Map> statements; - @Nullable private String missing; - - @NonNull public String id() { - return StringUtils.defaultString(id); - } - - @NonNull public Map labels() { - return labels != null ? labels : Collections.emptyMap(); - } - - @NonNull public Map descriptions() { - return descriptions != null ? descriptions : Collections.emptyMap(); - } - - @NonNull public Map sitelinks() { - return sitelinks != null ? sitelinks : Collections.emptyMap(); - } - - @Nullable - public Map> getStatements() { - return statements; - } - - boolean isMissing() { - return "-1".equals(id) && missing != null; - } - } - - public static class Label { - @Nullable private String language; - @Nullable private String value; - - public Label(@Nullable final String language, @Nullable final String value) { - this.language = language; - this.value = value; - } - - @NonNull public String language() { - return StringUtils.defaultString(language); - } - - @NonNull public String value() { - return StringUtils.defaultString(value); - } - } - - public static class SiteLink { - @Nullable private String site; - @Nullable private String title; - - @NonNull public String getSite() { - return StringUtils.defaultString(site); - } - - @NonNull public String getTitle() { - return StringUtils.defaultString(title); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt new file mode 100644 index 0000000000..588dbd262b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/Entities.kt @@ -0,0 +1,64 @@ +package fr.free.nrw.commons.wikidata.model + +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.wikidata.mwapi.MwResponse +import org.apache.commons.lang3.StringUtils + +class Entities : MwResponse() { + private val entities: Map? = null + val success: Int = 0 + + fun entities(): Map = entities ?: emptyMap() + + private val first : Entity? + get() = entities?.values?.iterator()?.next() + + override fun postProcess() { + first?.let { + if (it.isMissing()) throw RuntimeException("The requested entity was not found.") + } + } + + class Entity { + private val type: String? = null + private val id: String? = null + private val labels: Map? = null + private val descriptions: Map? = null + private val sitelinks: Map? = null + + @SerializedName(value = "statements", alternate = ["claims"]) + val statements: Map>? = null + private val missing: String? = null + + fun id(): String = + StringUtils.defaultString(id) + + fun labels(): Map = + labels ?: emptyMap() + + fun descriptions(): Map = + descriptions ?: emptyMap() + + fun sitelinks(): Map = + sitelinks ?: emptyMap() + + fun isMissing(): Boolean = + "-1" == id && missing != null + } + + class Label(private val language: String?, private val value: String?) { + fun language(): String = + StringUtils.defaultString(language) + + fun value(): String = + StringUtils.defaultString(value) + } + + class SiteLink { + val site: String? = null + get() = StringUtils.defaultString(field) + + private val title: String? = null + get() = StringUtils.defaultString(field) + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java b/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java deleted file mode 100644 index 204ea0ab46..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.java +++ /dev/null @@ -1,292 +0,0 @@ -package fr.free.nrw.commons.wikidata.model; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.TextUtils; - -import androidx.annotation.NonNull; - -import com.google.gson.annotations.SerializedName; - -import org.apache.commons.lang3.StringUtils; -import fr.free.nrw.commons.language.AppLanguageLookUpTable; - -/** - * The base URL and Wikipedia language code for a MediaWiki site. Examples: - * - *
    - * Name: scheme / authority / language code - *
  • English Wikipedia: HTTPS / en.wikipedia.org / en
  • - *
  • Chinese Wikipedia: HTTPS / zh.wikipedia.org / zh-hans or zh-hant
  • - *
  • Meta-Wiki: HTTPS / meta.wikimedia.org / (none)
  • - *
  • Test Wikipedia: HTTPS / test.wikipedia.org / test
  • - *
  • Võro Wikipedia: HTTPS / fiu-vro.wikipedia.org / fiu-vro
  • - *
  • Simple English Wikipedia: HTTPS / simple.wikipedia.org / simple
  • - *
  • Simple English Wikipedia (beta cluster mirror): HTTP / simple.wikipedia.beta.wmflabs.org / simple
  • - *
  • Development: HTTP / 192.168.1.11:8080 / (none)
  • - *
- * - * As shown above, the language code or mapping is part of the authority: - *
    - * Validity: authority / language code - *
  • Correct: "test.wikipedia.org" / "test"
  • - *
  • Correct: "wikipedia.org", ""
  • - *
  • Correct: "no.wikipedia.org", "nb"
  • - *
  • Incorrect: "wikipedia.org", "test"
  • - *
- */ -public class WikiSite implements Parcelable { - private static String WIKIPEDIA_URL = "https://wikipedia.org/"; - - public static final String DEFAULT_SCHEME = "https"; - private static String DEFAULT_BASE_URL = WIKIPEDIA_URL; - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - @Override - public WikiSite createFromParcel(Parcel in) { - return new WikiSite(in); - } - - @Override - public WikiSite[] newArray(int size) { - return new WikiSite[size]; - } - }; - - // todo: remove @SerializedName. this is now in the TypeAdapter and a "uri" case may be added - @SerializedName("domain") @NonNull private final Uri uri; - @NonNull private String languageCode; - - public static WikiSite forLanguageCode(@NonNull String languageCode) { - Uri uri = ensureScheme(Uri.parse(DEFAULT_BASE_URL)); - return new WikiSite((languageCode.isEmpty() - ? "" : (languageCodeToSubdomain(languageCode) + ".")) + uri.getAuthority(), - languageCode); - } - - public WikiSite(@NonNull Uri uri) { - Uri tempUri = ensureScheme(uri); - String authority = tempUri.getAuthority(); - if (("wikipedia.org".equals(authority) || "www.wikipedia.org".equals(authority)) - && tempUri.getPath() != null && tempUri.getPath().startsWith("/wiki")) { - // Special case for Wikipedia only: assume English subdomain when none given. - authority = "en.wikipedia.org"; - } - String langVariant = getLanguageVariantFromUri(tempUri); - if (!TextUtils.isEmpty(langVariant)) { - languageCode = langVariant; - } else { - languageCode = authorityToLanguageCode(authority); - } - this.uri = new Uri.Builder() - .scheme(tempUri.getScheme()) - .encodedAuthority(authority) - .build(); - } - - /** Get language variant code from a Uri, e.g. "zh-*", otherwise returns empty string. */ - @NonNull - private String getLanguageVariantFromUri(@NonNull Uri uri) { - if (TextUtils.isEmpty(uri.getPath())) { - return ""; - } - String[] parts = StringUtils.split(StringUtils.defaultString(uri.getPath()), '/'); - return parts.length > 1 && !parts[0].equals("wiki") ? parts[0] : ""; - } - - public WikiSite(@NonNull String url) { - this(url.startsWith("http") ? Uri.parse(url) : url.startsWith("//") - ? Uri.parse(DEFAULT_SCHEME + ":" + url) : Uri.parse(DEFAULT_SCHEME + "://" + url)); - } - - public WikiSite(@NonNull String authority, @NonNull String languageCode) { - this(authority); - this.languageCode = languageCode; - } - - @NonNull - public String scheme() { - return TextUtils.isEmpty(uri.getScheme()) ? DEFAULT_SCHEME : uri.getScheme(); - } - - /** - * @return The complete wiki authority including language subdomain but not including scheme, - * authentication, port, nor trailing slash. - * - * @see URL syntax - */ - @NonNull - public String authority() { - return uri.getAuthority(); - } - - /** - * Like {@link #authority()} but with a "m." between the language subdomain and the rest of the host. - * Examples: - * - *
    - *
  • English Wikipedia: en.m.wikipedia.org
  • - *
  • Chinese Wikipedia: zh.m.wikipedia.org
  • - *
  • Meta-Wiki: meta.m.wikimedia.org
  • - *
  • Test Wikipedia: test.m.wikipedia.org
  • - *
  • Võro Wikipedia: fiu-vro.m.wikipedia.org
  • - *
  • Simple English Wikipedia: simple.m.wikipedia.org
  • - *
  • Simple English Wikipedia (beta cluster mirror): simple.m.wikipedia.beta.wmflabs.org
  • - *
  • Development: m.192.168.1.11
  • - *
- */ - @NonNull - public String mobileAuthority() { - return authorityToMobile(authority()); - } - - /** - * Get wiki's mobile URL - * Eg. https://en.m.wikipedia.org - * @return - */ - public String mobileUrl() { - return String.format("%1$s://%2$s", scheme(), mobileAuthority()); - } - - @NonNull - public String subdomain() { - return languageCodeToSubdomain(languageCode); - } - - /** - * @return A path without an authority for the segment including a leading "/". - */ - @NonNull - public String path(@NonNull String segment) { - return "/w/" + segment; - } - - - @NonNull public Uri uri() { - return uri; - } - - /** - * @return The canonical URL. e.g., https://en.wikipedia.org. - */ - @NonNull public String url() { - return uri.toString(); - } - - /** - * @return The canonical URL for segment. e.g., https://en.wikipedia.org/w/foo. - */ - @NonNull public String url(@NonNull String segment) { - return url() + path(segment); - } - - /** - * @return The wiki language code which may differ from the language subdomain. Empty if - * language code is unknown. Ex: "en", "zh-hans", "" - * - * @see AppLanguageLookUpTable - */ - @NonNull - public String languageCode() { - return languageCode; - } - - // Auto-generated - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - WikiSite wiki = (WikiSite) o; - - if (!uri.equals(wiki.uri)) { - return false; - } - return languageCode.equals(wiki.languageCode); - } - - // Auto-generated - @Override - public int hashCode() { - int result = uri.hashCode(); - result = 31 * result + languageCode.hashCode(); - return result; - } - - // Auto-generated - @Override - public String toString() { - return "WikiSite{" - + "uri=" + uri - + ", languageCode='" + languageCode + '\'' - + '}'; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeParcelable(uri, 0); - dest.writeString(languageCode); - } - - protected WikiSite(@NonNull Parcel in) { - this.uri = in.readParcelable(Uri.class.getClassLoader()); - this.languageCode = in.readString(); - } - - @NonNull - private static String languageCodeToSubdomain(@NonNull String languageCode) { - switch (languageCode) { - case AppLanguageLookUpTable.SIMPLIFIED_CHINESE_LANGUAGE_CODE: - case AppLanguageLookUpTable.TRADITIONAL_CHINESE_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_CN_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_HK_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_MO_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_SG_LANGUAGE_CODE: - case AppLanguageLookUpTable.CHINESE_TW_LANGUAGE_CODE: - return AppLanguageLookUpTable.CHINESE_LANGUAGE_CODE; - case AppLanguageLookUpTable.NORWEGIAN_BOKMAL_LANGUAGE_CODE: - return AppLanguageLookUpTable.NORWEGIAN_LEGACY_LANGUAGE_CODE; // T114042 - default: - return languageCode; - } - } - - @NonNull private static String authorityToLanguageCode(@NonNull String authority) { - String[] parts = authority.split("\\."); - final int minLengthForSubdomain = 3; - if (parts.length < minLengthForSubdomain - || parts.length == minLengthForSubdomain && parts[0].equals("m")) { - // "" - // wikipedia.org - // m.wikipedia.org - return ""; - } - return parts[0]; - } - - @NonNull private static Uri ensureScheme(@NonNull Uri uri) { - if (TextUtils.isEmpty(uri.getScheme())) { - return uri.buildUpon().scheme(DEFAULT_SCHEME).build(); - } - return uri; - } - - /** @param authority Host and optional port. */ - @NonNull private String authorityToMobile(@NonNull String authority) { - if (authority.startsWith("m.") || authority.contains(".m.")) { - return authority; - } - return authority.replaceFirst("^" + subdomain() + "\\.?", "$0m."); - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt b/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt new file mode 100644 index 0000000000..1cd0bb8585 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/model/WikiSite.kt @@ -0,0 +1,269 @@ +package fr.free.nrw.commons.wikidata.model + +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.text.TextUtils +import com.google.gson.annotations.SerializedName +import fr.free.nrw.commons.language.AppLanguageLookUpTable +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_CN_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_HK_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_MO_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_SG_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.CHINESE_TW_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.NORWEGIAN_BOKMAL_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.NORWEGIAN_LEGACY_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.SIMPLIFIED_CHINESE_LANGUAGE_CODE +import fr.free.nrw.commons.language.AppLanguageLookUpTable.Companion.TRADITIONAL_CHINESE_LANGUAGE_CODE +import org.apache.commons.lang3.StringUtils +import java.util.Locale + +/** + * The base URL and Wikipedia language code for a MediaWiki site. Examples: + * + * + * Name: scheme / authority / language code + * * English Wikipedia: HTTPS / en.wikipedia.org / en + * * Chinese Wikipedia: HTTPS / zh.wikipedia.org / zh-hans or zh-hant + * * Meta-Wiki: HTTPS / meta.wikimedia.org / (none) + * * Test Wikipedia: HTTPS / test.wikipedia.org / test + * * Võro Wikipedia: HTTPS / fiu-vro.wikipedia.org / fiu-vro + * * Simple English Wikipedia: HTTPS / simple.wikipedia.org / simple + * * Simple English Wikipedia (beta cluster mirror): HTTP / simple.wikipedia.beta.wmflabs.org / simple + * * Development: HTTP / 192.168.1.11:8080 / (none) + * + * + * **As shown above, the language code or mapping is part of the authority:** + * + * Validity: authority / language code + * * Correct: "test.wikipedia.org" / "test" + * * Correct: "wikipedia.org", "" + * * Correct: "no.wikipedia.org", "nb" + * * Incorrect: "wikipedia.org", "test" + * + */ +class WikiSite : Parcelable { + //TODO: remove @SerializedName. this is now in the TypeAdapter and a "uri" case may be added + @SerializedName("domain") + private val uri: Uri + + private var languageCode: String? = null + + constructor(uri: Uri) { + val tempUri = ensureScheme(uri) + var authority = tempUri.authority + + if (authority.isWikipedia && tempUri.path?.startsWith("/wiki") == true) { + // Special case for Wikipedia only: assume English subdomain when none given. + authority = "en.wikipedia.org" + } + + val langVariant = getLanguageVariantFromUri(tempUri) + languageCode = if (!TextUtils.isEmpty(langVariant)) { + langVariant + } else { + authorityToLanguageCode(authority!!) + } + + this.uri = Uri.Builder() + .scheme(tempUri.scheme) + .encodedAuthority(authority) + .build() + } + + private val String?.isWikipedia: Boolean get() = + (this == "wikipedia.org" || this == "www.wikipedia.org") + + /** Get language variant code from a Uri, e.g. "zh-*", otherwise returns empty string. */ + private fun getLanguageVariantFromUri(uri: Uri): String { + if (TextUtils.isEmpty(uri.path)) { + return "" + } + val parts = StringUtils.split(StringUtils.defaultString(uri.path), '/') + return if (parts.size > 1 && parts[0] != "wiki") parts[0] else "" + } + + constructor(url: String) : this( + if (url.startsWith("http")) Uri.parse(url) else if (url.startsWith("//")) + Uri.parse("$DEFAULT_SCHEME:$url") + else + Uri.parse("$DEFAULT_SCHEME://$url") + ) + + constructor(authority: String, languageCode: String) : this(authority) { + this.languageCode = languageCode + } + + fun scheme(): String = + if (TextUtils.isEmpty(uri.scheme)) DEFAULT_SCHEME else uri.scheme!! + + /** + * @return The complete wiki authority including language subdomain but not including scheme, + * authentication, port, nor trailing slash. + * + * @see [URL syntax](https://en.wikipedia.org/wiki/Uniform_Resource_Locator.Syntax) + */ + fun authority(): String = uri.authority!! + + /** + * Like [.authority] but with a "m." between the language subdomain and the rest of the host. + * Examples: + * + * + * * English Wikipedia: en.m.wikipedia.org + * * Chinese Wikipedia: zh.m.wikipedia.org + * * Meta-Wiki: meta.m.wikimedia.org + * * Test Wikipedia: test.m.wikipedia.org + * * Võro Wikipedia: fiu-vro.m.wikipedia.org + * * Simple English Wikipedia: simple.m.wikipedia.org + * * Simple English Wikipedia (beta cluster mirror): simple.m.wikipedia.beta.wmflabs.org + * * Development: m.192.168.1.11 + * + */ + fun mobileAuthority(): String = authorityToMobile(authority()) + + /** + * Get wiki's mobile URL + * Eg. https://en.m.wikipedia.org + * @return + */ + fun mobileUrl(): String = String.format("%1\$s://%2\$s", scheme(), mobileAuthority()) + + fun subdomain(): String = languageCodeToSubdomain(languageCode!!) + + /** + * @return A path without an authority for the segment including a leading "/". + */ + fun path(segment: String): String = "/w/$segment" + + + fun uri(): Uri = uri + + /** + * @return The canonical URL. e.g., https://en.wikipedia.org. + */ + fun url(): String = uri.toString() + + /** + * @return The canonical URL for segment. e.g., https://en.wikipedia.org/w/foo. + */ + fun url(segment: String): String = url() + path(segment) + + /** + * @return The wiki language code which may differ from the language subdomain. Empty if + * language code is unknown. Ex: "en", "zh-hans", "" + * + * @see AppLanguageLookUpTable + */ + fun languageCode(): String = languageCode!! + + // Auto-generated + override fun equals(o: Any?): Boolean { + if (this === o) { + return true + } + if (o == null || javaClass != o.javaClass) { + return false + } + + val wiki = o as WikiSite + + if (uri != wiki.uri) { + return false + } + return languageCode == wiki.languageCode + } + + // Auto-generated + override fun hashCode(): Int { + var result = uri.hashCode() + result = 31 * result + languageCode.hashCode() + return result + } + + // Auto-generated + override fun toString(): String { + return ("WikiSite{" + + "uri=" + uri + + ", languageCode='" + languageCode + '\'' + + '}') + } + + override fun describeContents(): Int = 0 + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeParcelable(uri, 0) + dest.writeString(languageCode) + } + + protected constructor(`in`: Parcel) { + uri = `in`.readParcelable(Uri::class.java.classLoader)!! + languageCode = `in`.readString() + } + + /** @param authority Host and optional port. + */ + private fun authorityToMobile(authority: String): String { + if (authority.startsWith("m.") || authority.contains(".m.")) { + return authority + } + return authority.replaceFirst(("^" + subdomain() + "\\.?").toRegex(), "$0m.") + } + + companion object { + const val WIKIPEDIA_URL = "https://wikipedia.org/" + const val DEFAULT_SCHEME: String = "https" + + @JvmField + val CREATOR: Parcelable.Creator = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): WikiSite { + return WikiSite(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + fun forDefaultLocaleLanguageCode(): WikiSite { + val languageCode: String = Locale.getDefault().language + val subdomain = if (languageCode.isEmpty()) "" else languageCodeToSubdomain(languageCode) + "." + val uri = ensureScheme(Uri.parse(WIKIPEDIA_URL)) + return WikiSite(subdomain + uri.authority, languageCode) + } + + private fun languageCodeToSubdomain(languageCode: String): String = when (languageCode) { + SIMPLIFIED_CHINESE_LANGUAGE_CODE, + TRADITIONAL_CHINESE_LANGUAGE_CODE, + CHINESE_CN_LANGUAGE_CODE, + CHINESE_HK_LANGUAGE_CODE, + CHINESE_MO_LANGUAGE_CODE, + CHINESE_SG_LANGUAGE_CODE, + CHINESE_TW_LANGUAGE_CODE -> CHINESE_LANGUAGE_CODE + + NORWEGIAN_BOKMAL_LANGUAGE_CODE -> NORWEGIAN_LEGACY_LANGUAGE_CODE // T114042 + + else -> languageCode + } + + private fun authorityToLanguageCode(authority: String): String { + val parts = authority.split("\\.".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val minLengthForSubdomain = 3 + if (parts.size < minLengthForSubdomain || parts.size == minLengthForSubdomain && parts[0] == "m") { + // "" + // wikipedia.org + // m.wikipedia.org + return "" + } + return parts[0] + } + + private fun ensureScheme(uri: Uri): Uri { + if (TextUtils.isEmpty(uri.scheme)) { + return uri.buildUpon().scheme(DEFAULT_SCHEME).build() + } + return uri + } + } +} From a8387f01c9e7d54b4f6cd7b7da342c64f38de352 Mon Sep 17 00:00:00 2001 From: Neel Doshi <60827173+neeldoshii@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:45:47 +0530 Subject: [PATCH 052/231] Bug Fixs & Enhancement of Achievement Screen (#5666) * Rename AchievementFragment from `.java` to `.kt` * Migrated AchievementFragment to kotlin * Revamped Achievement Screen * fixed AchievementFragment Unit Test * fixed Level on MoreBottomSheetFragment * Implemented Badge and Minor Code Refactor * Fixed the badge issue & made the badge clickable * Removed Redundant XML Code & Converted badges to green color and added values inside it * Fixed : showSnackBarWithRetry Test * Fixed : Theme issues on Light Mode --------- Co-authored-by: Nicolas Raoul --- app/build.gradle | 2 +- .../commons/navtab/MoreBottomSheetFragment.kt | 14 +- .../nrw/commons/profile/ProfileActivity.java | 1 - .../achievements/AchievementsFragment.java | 492 --------- .../achievements/AchievementsFragment.kt | 566 ++++++++++ .../main/res/layout/fragment_achievements.xml | 990 +++++++----------- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/styles.xml | 5 +- .../AchievementsFragmentUnitTests.kt | 2 +- 9 files changed, 944 insertions(+), 1132 deletions(-) delete mode 100644 app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java create mode 100644 app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt diff --git a/app/build.gradle b/app/build.gradle index 468255d38c..b83f2b01ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -47,7 +47,7 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' - implementation "com.google.android.material:material:1.9.0" + implementation "com.google.android.material:material:1.12.0" implementation 'com.karumi:dexter:5.0.0' implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' diff --git a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt index 857e18ec31..cbdf5f0875 100644 --- a/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/navtab/MoreBottomSheetFragment.kt @@ -111,10 +111,18 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() { private fun setUserName() { val store = BasicKvStore(requireContext(), getUserName()) val level = store.getString("userAchievementsLevel", "0") - binding?.moreProfile?.text = if (level == "0") { - "${getUserName()} (${getString(R.string.see_your_achievements)})" + if (level == "0"){ + binding?.moreProfile?.text = getString( + R.string.profileLevel, + getUserName(), + getString(R.string.see_your_achievements) // Second argument + ) } else { - "${getUserName()} (${getString(R.string.level)} $level)" + binding?.moreProfile?.text = getString( + R.string.profileLevel, + getUserName(), + level + ) } } diff --git a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java index 60a0f47a1a..390768416f 100644 --- a/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/profile/ProfileActivity.java @@ -16,7 +16,6 @@ import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; -import com.google.android.material.tabs.TabLayout; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.ViewPagerAdapter; diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java deleted file mode 100644 index ef6a323b2e..0000000000 --- a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.java +++ /dev/null @@ -1,492 +0,0 @@ -package fr.free.nrw.commons.profile.achievements; - -import android.accounts.Account; -import android.content.Context; -import android.net.Uri; -import android.os.Bundle; -import android.util.DisplayMetrics; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.Toast; -import androidx.annotation.Nullable; -import androidx.appcompat.view.ContextThemeWrapper; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; -import fr.free.nrw.commons.R; -import fr.free.nrw.commons.Utils; -import fr.free.nrw.commons.auth.SessionManager; -import fr.free.nrw.commons.databinding.FragmentAchievementsBinding; -import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; -import fr.free.nrw.commons.kvstore.BasicKvStore; -import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; -import fr.free.nrw.commons.utils.ConfigUtils; -import fr.free.nrw.commons.utils.DialogUtil; -import fr.free.nrw.commons.utils.ViewUtil; -import fr.free.nrw.commons.profile.ProfileActivity; -import io.reactivex.android.schedulers.AndroidSchedulers; -import io.reactivex.disposables.CompositeDisposable; -import io.reactivex.schedulers.Schedulers; -import java.util.Locale; -import java.util.Objects; -import javax.inject.Inject; -import org.apache.commons.lang3.StringUtils; -import timber.log.Timber; - -/** - * fragment for sharing feedback on uploaded activity - */ -public class AchievementsFragment extends CommonsDaggerSupportFragment { - - private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4; - private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; - - /** - * Help link URLs - */ - private static final String IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope"; - private static final String IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion"; - private static final String IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images"; - private static final String IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18"; - private static final String IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"; - private static final String QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images"; - private static final String THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks"; - - private LevelController.LevelInfo levelInfo; - - @Inject - SessionManager sessionManager; - - @Inject - OkHttpJsonApiClient okHttpJsonApiClient; - - private FragmentAchievementsBinding binding; - - private CompositeDisposable compositeDisposable = new CompositeDisposable(); - - // To keep track of the number of wiki edits made by a user - private int numberOfEdits = 0; - - private String userName; - - @Override - public void onCreate(@Nullable final Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (getArguments() != null) { - userName = getArguments().getString(ProfileActivity.KEY_USERNAME); - } - } - - /** - * This method helps in the creation Achievement screen and - * dynamically set the size of imageView - * - * @param savedInstanceState Data bundle - */ - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - binding = FragmentAchievementsBinding.inflate(inflater, container, false); - View rootView = binding.getRoot(); - - binding.achievementInfo.setOnClickListener(view -> showInfoDialog()); - binding.imagesUploadInfo.setOnClickListener(view -> showUploadInfo()); - binding.imagesRevertedInfo.setOnClickListener(view -> showRevertedInfo()); - binding.imagesUsedByWikiInfo.setOnClickListener(view -> showUsedByWikiInfo()); - binding.imagesNearbyInfo.setOnClickListener(view -> showImagesViaNearbyInfo()); - binding.imagesFeaturedInfo.setOnClickListener(view -> showFeaturedImagesInfo()); - binding.thanksReceivedInfo.setOnClickListener(view -> showThanksReceivedInfo()); - binding.qualityImagesInfo.setOnClickListener(view -> showQualityImagesInfo()); - - // DisplayMetrics used to fetch the size of the screen - DisplayMetrics displayMetrics = new DisplayMetrics(); - getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); - int height = displayMetrics.heightPixels; - int width = displayMetrics.widthPixels; - - // Used for the setting the size of imageView at runtime - ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) - binding.achievementBadgeImage.getLayoutParams(); - params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); - params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); - binding.achievementBadgeImage.requestLayout(); - binding.progressBar.setVisibility(View.VISIBLE); - - setHasOptionsMenu(true); - - // Set the initial value of WikiData edits to 0 - binding.wikidataEdits.setText("0"); - if(sessionManager.getUserName() == null || sessionManager.getUserName().equals(userName)){ - binding.tvAchievementsOfUser.setVisibility(View.GONE); - }else{ - binding.tvAchievementsOfUser.setVisibility(View.VISIBLE); - binding.tvAchievementsOfUser.setText(getString(R.string.achievements_of_user,userName)); - } - - // Achievements currently unimplemented in Beta flavor. Skip all API calls. - if(ConfigUtils.isBetaFlavour()) { - binding.progressBar.setVisibility(View.GONE); - binding.imagesUsedByWikiText.setText(R.string.no_image); - binding.imagesRevertedText.setText(R.string.no_image_reverted); - binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); - binding.wikidataEdits.setText("0"); - binding.imageFeatured.setText("0"); - binding.qualityImages.setText("0"); - binding.achievementLevel.setText("0"); - setMenuVisibility(true); - return rootView; - } - setWikidataEditCount(); - setAchievements(); - return rootView; - } - - @Override - public void onDestroyView() { - binding = null; - super.onDestroyView(); - } - - @Override - public void setMenuVisibility(boolean visible) { - super.setMenuVisibility(visible); - - // Whenever this fragment is revealed in a menu, - // notify Beta users the page data is unavailable - if(ConfigUtils.isBetaFlavour() && visible) { - Context ctx = null; - if(getContext() != null) { - ctx = getContext(); - } else if(getView() != null && getView().getContext() != null) { - ctx = getView().getContext(); - } - if(ctx != null) { - Toast.makeText(ctx, - R.string.achievements_unavailable_beta, - Toast.LENGTH_LONG).show(); - } - } - } - - /** - * To invoke the AlertDialog on clicking info button - */ - protected void showInfoDialog(){ - launchAlert( - getResources().getString(R.string.Achievements), - getResources().getString(R.string.achievements_info_message)); - } - - /** - * To call the API to get results in form Single - * which then calls parseJson when results are fetched - */ - private void setAchievements() { - binding.progressBar.setVisibility(View.VISIBLE); - if (checkAccount()) { - try{ - - compositeDisposable.add(okHttpJsonApiClient - .getAchievements(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - response -> { - if (response != null) { - setUploadCount(Achievements.from(response)); - } else { - Timber.d("success"); - binding.layoutImageReverts.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - // If the number of edits made by the user are more than 150,000 - // in some cases such high number of wiki edit counts cause the - // achievements calculator to fail in some cases, for more details - // refer Issue: #3295 - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - }, - t -> { - Timber.e(t, "Fetching achievements statistics failed"); - if (numberOfEdits <= 150000) { - showSnackBarWithRetry(false); - } else { - showSnackBarWithRetry(true); - } - } - )); - } - catch (Exception e){ - Timber.d(e+"success"); - } - } - } - - /** - * To call the API to fetch the count of wiki data edits - * in the form of JavaRx Single object - */ - private void setWikidataEditCount() { - if (StringUtils.isBlank(userName)) { - return; - } - compositeDisposable.add(okHttpJsonApiClient - .getWikidataEdits(userName) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(edits -> { - numberOfEdits = edits; - binding.wikidataEdits.setText(String.valueOf(edits)); - }, e -> { - Timber.e("Error:" + e); - })); - } - - /** - * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the - * listener passed - * @param tooManyAchievements if this value is true it means that the number of achievements of the - * user are so high that it wrecks havoc with the Achievements calculator due to which request may time - * out. Well this is the Ultimate Achievement - */ - private void showSnackBarWithRetry(boolean tooManyAchievements) { - if (tooManyAchievements) { - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); - } else { - binding.progressBar.setVisibility(View.GONE); - ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), - R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); - } - } - - /** - * Shows a generic error toast when error occurs while loading achievements or uploads - */ - private void onError() { - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); - binding.progressBar.setVisibility(View.GONE); - } - - /** - * used to the count of images uploaded by user - */ - private void setUploadCount(Achievements achievements) { - if (checkAccount()) { - compositeDisposable.add(okHttpJsonApiClient - .getUploadCount(Objects.requireNonNull(userName)) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> setAchievementsUploadCount(achievements, uploadCount), - t -> { - Timber.e(t, "Fetching upload count failed"); - onError(); - } - )); - } - } - - /** - * used to set achievements upload count and call hideProgressbar - * @param uploadCount - */ - private void setAchievementsUploadCount(Achievements achievements, int uploadCount) { - // Create a new instance of Achievements with updated imagesUploaded - Achievements updatedAchievements = new Achievements( - achievements.getUniqueUsedImages(), - achievements.getArticlesUsingImages(), - achievements.getThanksReceived(), - achievements.getFeaturedImages(), - achievements.getQualityImages(), - uploadCount, // Update imagesUploaded with new value - achievements.getRevertCount() - ); - - hideProgressBar(updatedAchievements); - } - - /** - * used to the uploaded images progressbar - * @param uploadCount - */ - private void setUploadProgress(int uploadCount){ - if (uploadCount==0){ - setZeroAchievements(); - }else { - binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); - binding.imagesUploadedProgressbar.setProgress - (100*uploadCount/levelInfo.getMaxUploadCount()); - binding.tvUploadedImages.setText - (uploadCount + "/" + levelInfo.getMaxUploadCount()); - } - - } - - private void setZeroAchievements() { - String message = !Objects.equals(sessionManager.getUserName(), userName) ? - getString(R.string.no_achievements_yet, userName) : - getString(R.string.you_have_no_achievements_yet); - DialogUtil.showAlertDialog(getActivity(), - null, - message, - getString(R.string.ok), - () -> {}, - true); -// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); -// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); -// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - binding.imagesUsedByWikiText.setText(R.string.no_image); - binding.imagesRevertedText.setText(R.string.no_image_reverted); - binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); - binding.achievementBadgeImage.setVisibility(View.INVISIBLE); - } - - /** - * used to set the non revert image percentage - * @param notRevertPercentage - */ - private void setImageRevertPercentage(int notRevertPercentage){ - binding.imageRevertsProgressbar.setVisibility(View.VISIBLE); - binding.imageRevertsProgressbar.setProgress(notRevertPercentage); - final String revertPercentage = Integer.toString(notRevertPercentage); - binding.tvRevertedImages.setText(revertPercentage + "%"); - binding.imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); - } - - /** - * Used the inflate the fetched statistics of the images uploaded by user - * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu - * @param achievements - */ - private void inflateAchievements(Achievements achievements) { -// binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); - binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); - binding.imagesUsedByWikiProgressBar.setProgress - (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); - binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/" - + levelInfo.getMaxUniqueImages()); - binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); - binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); - String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); - levelUpInfoString += " " + levelInfo.getLevelNumber(); - binding.achievementLevel.setText(levelUpInfoString); - binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, - new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); - binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber())); - BasicKvStore store = new BasicKvStore(this.getContext(), userName); - store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber())); - } - - /** - * to hide progressbar - */ - private void hideProgressBar(Achievements achievements) { - if (binding.progressBar != null) { - levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), - achievements.getUniqueUsedImages(), - achievements.getNotRevertPercentage()); - inflateAchievements(achievements); - setUploadProgress(achievements.getImagesUploaded()); - setImageRevertPercentage(achievements.getNotRevertPercentage()); - binding.progressBar.setVisibility(View.GONE); - } - } - - protected void showUploadInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.images_uploaded), - getResources().getString(R.string.images_uploaded_explanation), - IMAGES_UPLOADED_URL); - } - - protected void showRevertedInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.image_reverts), - getResources().getString(R.string.images_reverted_explanation), - IMAGES_REVERT_URL); - } - - protected void showUsedByWikiInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.images_used_by_wiki), - getResources().getString(R.string.images_used_explanation), - IMAGES_USED_URL); - } - - protected void showImagesViaNearbyInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_wikidata_edits), - getResources().getString(R.string.images_via_nearby_explanation), - IMAGES_NEARBY_PLACES_URL); - } - - protected void showFeaturedImagesInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_featured), - getResources().getString(R.string.images_featured_explanation), - IMAGES_FEATURED_URL); - } - - protected void showThanksReceivedInfo(){ - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_thanks), - getResources().getString(R.string.thanks_received_explanation), - THANKS_URL); - } - - public void showQualityImagesInfo() { - launchAlertWithHelpLink( - getResources().getString(R.string.statistics_quality), - getResources().getString(R.string.quality_images_info), - QUALITY_IMAGE_URL); - } - - /** - * takes title and message as input to display alerts - * @param title - * @param message - */ - private void launchAlert(String title, String message){ - DialogUtil.showAlertDialog(getActivity(), - title, - message, - getString(R.string.ok), - () -> {}, - true); - } - - /** - * Launch Alert with a READ MORE button and clicking it open a custom webpage - */ - private void launchAlertWithHelpLink(String title, String message, String helpLinkUrl) { - DialogUtil.showAlertDialog(getActivity(), - title, - message, - getString(R.string.ok), - getString(R.string.read_help_link), - () -> {}, - () -> Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)), - null, - true); - } - - /** - * check to ensure that user is logged in - * @return - */ - private boolean checkAccount(){ - Account currentAccount = sessionManager.getCurrentAccount(); - if (currentAccount == null) { - Timber.d("Current account is null"); - ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in)); - sessionManager.forceLogin(getActivity()); - return false; - } - return true; - } -} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt new file mode 100644 index 0000000000..020a67f24f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/profile/achievements/AchievementsFragment.kt @@ -0,0 +1,566 @@ +package fr.free.nrw.commons.profile.achievements + +import android.net.Uri +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.Toast +import androidx.appcompat.view.ContextThemeWrapper +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat +import com.google.android.material.badge.BadgeDrawable +import com.google.android.material.badge.BadgeUtils +import com.google.android.material.badge.ExperimentalBadgeUtils +import fr.free.nrw.commons.R +import fr.free.nrw.commons.Utils +import fr.free.nrw.commons.auth.SessionManager +import fr.free.nrw.commons.databinding.FragmentAchievementsBinding +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment +import fr.free.nrw.commons.kvstore.BasicKvStore +import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient +import fr.free.nrw.commons.profile.ProfileActivity +import fr.free.nrw.commons.profile.achievements.LevelController.LevelInfo.Companion.from +import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour +import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog +import fr.free.nrw.commons.utils.ViewUtil.showDismissibleSnackBar +import fr.free.nrw.commons.utils.ViewUtil.showLongToast +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import org.apache.commons.lang3.StringUtils +import timber.log.Timber +import java.util.Objects +import javax.inject.Inject + +class AchievementsFragment : CommonsDaggerSupportFragment(){ + private lateinit var levelInfo: LevelController.LevelInfo + + @Inject + lateinit var sessionManager: SessionManager + + @Inject + lateinit var okHttpJsonApiClient: OkHttpJsonApiClient + + private var _binding: FragmentAchievementsBinding? = null + private val binding get() = _binding!! + // To keep track of the number of wiki edits made by a user + private var numberOfEdits: Int = 0 + + private var userName: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + userName = it.getString(ProfileActivity.KEY_USERNAME) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentAchievementsBinding.inflate(inflater, container, false) + + binding.achievementInfo.setOnClickListener { showInfoDialog() } + binding.imagesUploadInfoIcon.setOnClickListener { showUploadInfo() } + binding.imagesRevertedInfoIcon.setOnClickListener { showRevertedInfo() } + binding.imagesUsedByWikiInfoIcon.setOnClickListener { showUsedByWikiInfo() } + binding.wikidataEditsIcon.setOnClickListener { showImagesViaNearbyInfo() } + binding.featuredImageIcon.setOnClickListener { showFeaturedImagesInfo() } + binding.thanksImageIcon.setOnClickListener { showThanksReceivedInfo() } + binding.qualityImageIcon.setOnClickListener { showQualityImagesInfo() } + + // DisplayMetrics used to fetch the size of the screen + val displayMetrics = DisplayMetrics() + requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) + val height = displayMetrics.heightPixels + val width = displayMetrics.widthPixels + + // Used for the setting the size of imageView at runtime + // TODO REMOVE + val params = binding.achievementBadgeImage.layoutParams as ConstraintLayout.LayoutParams + params.height = (height * BADGE_IMAGE_HEIGHT_RATIO).toInt() + params.width = (width * BADGE_IMAGE_WIDTH_RATIO).toInt() + binding.achievementBadgeImage.requestLayout() + binding.progressBar.visibility = View.VISIBLE + + setHasOptionsMenu(true) + if (sessionManager.userName == null || sessionManager.userName == userName) { + binding.tvAchievementsOfUser.visibility = View.GONE + } else { + binding.tvAchievementsOfUser.visibility = View.VISIBLE + binding.tvAchievementsOfUser.text = getString(R.string.achievements_of_user, userName) + } + if (isBetaFlavour) { + binding.layout.visibility = View.GONE + setMenuVisibility(true) + return binding.root + } + + + setWikidataEditCount() + setAchievements() + return binding.root + + } + + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + + override fun setMenuVisibility(visible: Boolean) { + super.setMenuVisibility(visible) + + // Whenever this fragment is revealed in a menu, + // notify Beta users the page data is unavailable + if (isBetaFlavour && visible) { + val ctx = context ?: view?.context + ctx?.let { + Toast.makeText(it, R.string.achievements_unavailable_beta, Toast.LENGTH_LONG).show() + } + } + } + + /** + * To invoke the AlertDialog on clicking info button + */ + fun showInfoDialog() { + launchAlert( + resources.getString(R.string.Achievements), + resources.getString(R.string.achievements_info_message) + ) + } + + + + + /** + * To call the API to get results in form Single + * which then calls parseJson when results are fetched + */ + + private fun setAchievements() { + binding.progressBar.visibility = View.VISIBLE + if (checkAccount()) { + try { + compositeDisposable.add( + okHttpJsonApiClient + .getAchievements(userName ?: return) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { response -> + if (response != null) { + setUploadCount(Achievements.from(response)) + } else { + Timber.d("Success") + // TODO Create a Method to Hide all the Statistics +// binding.layoutImageReverts.visibility = View.INVISIBLE +// binding.achievementBadgeImage.visibility = View.INVISIBLE + // If the number of edits made by the user are more than 150,000 + // in some cases such high number of wiki edit counts cause the + // achievements calculator to fail in some cases, for more details + // refer Issue: #3295 + if (numberOfEdits <= 150_000) { + showSnackBarWithRetry(false) + } else { + showSnackBarWithRetry(true) + } + } + }, + { throwable -> + Timber.e(throwable, "Fetching achievements statistics failed") + if (numberOfEdits <= 150_000) { + showSnackBarWithRetry(false) + } else { + showSnackBarWithRetry(true) + } + } + ) + ) + } catch (e: Exception) { + Timber.d("Exception: ${e.message}") + } + } + } + + /** + * To call the API to fetch the count of wiki data edits + * in the form of JavaRx Single object + */ + + private fun setWikidataEditCount() { + if (StringUtils.isBlank(userName)) { + return + } + compositeDisposable.add( + okHttpJsonApiClient + .getWikidataEdits(userName) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ edits: Int -> + numberOfEdits = edits + showBadgesWithCount(view = binding.wikidataEditsIcon, count = edits) + }, { e: Throwable -> + Timber.e("Error:$e") + }) + ) + } + + /** + * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the + * listener passed + * @param tooManyAchievements if this value is true it means that the number of achievements of the + * user are so high that it wrecks havoc with the Achievements calculator due to which request may time + * out. Well this is the Ultimate Achievement + */ + private fun showSnackBarWithRetry(tooManyAchievements: Boolean) { + if (tooManyAchievements) { + if (view == null) { + return + } + else { + binding.progressBar.visibility = View.GONE + showDismissibleSnackBar( + requireView().findViewById(android.R.id.content), + R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry + ) { setAchievements() } + } + + } else { + if (view == null) { + return + } + binding.progressBar.visibility = View.GONE + showDismissibleSnackBar( + requireView().findViewById(android.R.id.content), + R.string.achievements_fetch_failed, R.string.retry + ) { setAchievements() } + } + } + + /** + * Shows a generic error toast when error occurs while loading achievements or uploads + */ + private fun onError() { + showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) + binding.progressBar.visibility = View.GONE + } + + /** + * used to the count of images uploaded by user + */ + + private fun setUploadCount(achievements: Achievements) { + if (checkAccount()) { + compositeDisposable.add(okHttpJsonApiClient + .getUploadCount(Objects.requireNonNull(userName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { uploadCount: Int? -> + setAchievementsUploadCount( + achievements, + uploadCount ?:0 + ) + }, + { t: Throwable? -> + Timber.e(t, "Fetching upload count failed") + onError() + } + )) + } + } + + /** + * used to set achievements upload count and call hideProgressbar + * @param uploadCount + */ + private fun setAchievementsUploadCount(achievements: Achievements, uploadCount: Int) { + // Create a new instance of Achievements with updated imagesUploaded + val updatedAchievements = Achievements( + achievements.uniqueUsedImages, + achievements.articlesUsingImages, + achievements.thanksReceived, + achievements.featuredImages, + achievements.qualityImages, + uploadCount, // Update imagesUploaded with new value + achievements.revertCount + ) + + hideProgressBar(updatedAchievements) + } + + /** + * used to the uploaded images progressbar + * @param uploadCount + */ + private fun setUploadProgress(uploadCount: Int) { + if (uploadCount == 0) { + setZeroAchievements() + } else { + binding.imagesUploadedProgressbar.visibility = View.VISIBLE + binding.imagesUploadedProgressbar.progress = + 100 * uploadCount / levelInfo.maxUploadCount + binding.imageUploadedTVCount.text = uploadCount.toString() + "/" + levelInfo.maxUploadCount + } + } + + private fun setZeroAchievements() { + val message = if (sessionManager.userName != userName) { + getString(R.string.no_achievements_yet, userName ) + } else { + getString(R.string.you_have_no_achievements_yet) + } + showAlertDialog( + requireActivity(), + null, + message, + getString(R.string.ok), + {}, + true + ) + +// binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); +// binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); +// binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); + //binding.achievementBadgeImage.visibility = View.INVISIBLE // TODO + binding.imagesUsedCount.setText(R.string.no_image) + binding.imagesRevertedText.setText(R.string.no_image_reverted) + binding.imagesUploadTextParam.setText(R.string.no_image_uploaded) + } + + /** + * used to set the non revert image percentage + * @param notRevertPercentage + */ + private fun setImageRevertPercentage(notRevertPercentage: Int) { + binding.imageRevertsProgressbar.visibility = View.VISIBLE + binding.imageRevertsProgressbar.progress = notRevertPercentage + val revertPercentage = notRevertPercentage.toString() + binding.imageRevertTVCount.text = "$revertPercentage%" + binding.imagesRevertLimitText.text = + resources.getString(R.string.achievements_revert_limit_message) + levelInfo.minNonRevertPercentage + "%" + } + + /** + * Used the inflate the fetched statistics of the images uploaded by user + * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu + * @param achievements + */ + private fun inflateAchievements(achievements: Achievements) { + + // Thanks Received Badge + showBadgesWithCount(view = binding.thanksImageIcon, count = achievements.thanksReceived) + + // Featured Images Badge + showBadgesWithCount(view = binding.featuredImageIcon, count = achievements.featuredImages) + + // Quality Images Badge + showBadgesWithCount(view = binding.qualityImageIcon, count = achievements.qualityImages) + + binding.imagesUsedByWikiProgressBar.progress = + 100 * achievements.uniqueUsedImages / levelInfo.maxUniqueImages + binding.imagesUsedCount.text = (achievements.uniqueUsedImages.toString() + "/" + + levelInfo.maxUniqueImages) + + binding.achievementLevel.text = getString(R.string.level,levelInfo.levelNumber) + binding.achievementBadgeImage.setImageDrawable( + VectorDrawableCompat.create( + resources, R.drawable.badge, + ContextThemeWrapper(activity, levelInfo.levelStyle).theme + ) + ) + binding.achievementBadgeText.text = levelInfo.levelNumber.toString() + val store = BasicKvStore(requireContext(), userName) + store.putString("userAchievementsLevel", levelInfo.levelNumber.toString()) + } + + /** + * This function is used to show badge on any view (button, imageView, etc) + * @param view The View on which the badge will be displayed eg (button, imageView, etc) + * @param count The number to be displayed inside the badge. + * @param backgroundColor The badge background color. Default is R.attr.colorPrimary + * @param badgeTextColor The badge text color. Default is R.attr.colorPrimary + * @param badgeGravity The position of the badge [TOP_END,TOP_START,BOTTOM_END,BOTTOM_START]. Default is TOP_END + * @return if the number is 0, then it will not create badge for it and hide the view + * @see https://developer.android.com/reference/com/google/android/material/badge/BadgeDrawable + */ + + private fun showBadgesWithCount( + view: View, + count: Int, + backgroundColor: Int = R.attr.colorPrimary, + badgeTextColor: Int = R.attr.textEnabled, + badgeGravity: Int = BadgeDrawable.TOP_END + ) { + //https://stackoverflow.com/a/67742035 + if (count == 0) { + view.visibility = View.GONE + return + } + + view.viewTreeObserver.addOnGlobalLayoutListener(object : + ViewTreeObserver.OnGlobalLayoutListener { + /** + * Callback method to be invoked when the global layout state or the visibility of views + * within the view tree changes + */ + @ExperimentalBadgeUtils + override fun onGlobalLayout() { + view.visibility = View.VISIBLE + val badgeDrawable = BadgeDrawable.create(requireActivity()) + badgeDrawable.number = count + badgeDrawable.badgeGravity = badgeGravity + badgeDrawable.badgeTextColor = badgeTextColor + badgeDrawable.backgroundColor = backgroundColor + BadgeUtils.attachBadgeDrawable(badgeDrawable, view) + view.getViewTreeObserver().removeOnGlobalLayoutListener(this) + } + }) + } + + /** + * to hide progressbar + */ + private fun hideProgressBar(achievements: Achievements) { + if (binding.progressBar != null) { + levelInfo = from( + achievements.imagesUploaded, + achievements.uniqueUsedImages, + achievements.notRevertPercentage + ) + inflateAchievements(achievements) + setUploadProgress(achievements.imagesUploaded) + setImageRevertPercentage(achievements.notRevertPercentage) + binding.progressBar.visibility = View.GONE + } + } + + fun showUploadInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.images_uploaded), + resources.getString(R.string.images_uploaded_explanation), + IMAGES_UPLOADED_URL + ) + } + + fun showRevertedInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.image_reverts), + resources.getString(R.string.images_reverted_explanation), + IMAGES_REVERT_URL + ) + } + + fun showUsedByWikiInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.images_used_by_wiki), + resources.getString(R.string.images_used_explanation), + IMAGES_USED_URL + ) + } + + fun showImagesViaNearbyInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_wikidata_edits), + resources.getString(R.string.images_via_nearby_explanation), + IMAGES_NEARBY_PLACES_URL + ) + } + + fun showFeaturedImagesInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_featured), + resources.getString(R.string.images_featured_explanation), + IMAGES_FEATURED_URL + ) + } + + fun showThanksReceivedInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_thanks), + resources.getString(R.string.thanks_received_explanation), + THANKS_URL + ) + } + + fun showQualityImagesInfo() { + launchAlertWithHelpLink( + resources.getString(R.string.statistics_quality), + resources.getString(R.string.quality_images_info), + QUALITY_IMAGE_URL + ) + } + + /** + * takes title and message as input to display alerts + * @param title + * @param message + */ + private fun launchAlert(title: String, message: String) { + showAlertDialog( + requireActivity(), + title, + message, + getString(R.string.ok), + {}, + true + ) + } + + /** + * Launch Alert with a READ MORE button and clicking it open a custom webpage + */ + private fun launchAlertWithHelpLink(title: String, message: String, helpLinkUrl: String) { + showAlertDialog( + requireActivity(), + title, + message, + getString(R.string.ok), + getString(R.string.read_help_link), + {}, + { Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) }, + null, + true + ) + } + /** + * check to ensure that user is logged in + * @return + */ + private fun checkAccount(): Boolean { + val currentAccount = sessionManager.currentAccount + if (currentAccount == null) { + Timber.d("Current account is null") + showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) + sessionManager.forceLogin(activity) + return false + } + return true + } + + + + companion object{ + private const val BADGE_IMAGE_WIDTH_RATIO = 0.4 + private const val BADGE_IMAGE_HEIGHT_RATIO = 0.3 + + /** + * Help link URLs + */ + private const val IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope" + private const val IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion" + private const val IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images" + private const val IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18" + private const val IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures" + private const val QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images" + private const val THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_achievements.xml b/app/src/main/res/layout/fragment_achievements.xml index e0dddcf5bf..00c18b3232 100644 --- a/app/src/main/res/layout/fragment_achievements.xml +++ b/app/src/main/res/layout/fragment_achievements.xml @@ -1,640 +1,368 @@ - - - + + + + + + + + + - - + + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_centerInParent="true" + android:progressDrawable="@android:drawable/progress_horizontal" + android:progressBackgroundTintMode="multiply" + android:progressTint="#5ce65c" + tools:progress="50" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 187c5fc96f..fa21736990 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -371,11 +371,13 @@ Delete Achievements Profile + Badges Statistics Thanks Received Featured Images Images via \"Nearby Places\" - Level + Level %d + %s (Level %s) Images Uploaded Images Not Reverted Images Used diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 94856e4eb6..67b5eae0f5 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - -