diff --git a/app/build.gradle b/app/build.gradle index 59308bbaf..dc803f087 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,12 +100,12 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION" //Mocking - testImplementation("io.mockk:mockk:1.13.4") testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0' testImplementation 'org.mockito:mockito-inline:5.2.0' 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' @@ -374,11 +374,11 @@ android { compileOptions { - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = "11" + jvmTarget = "17" } buildToolsVersion buildToolsVersion 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 2f05705ba..40f360a24 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 @@ public class LocationPickerActivity extends BaseActivity implements */ 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 class LocationPickerActivity extends BaseActivity implements } 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/campaigns/CampaignView.java b/app/src/main/java/fr/free/nrw/commons/campaigns/CampaignView.java index 4d1eb33ce..d1ee4c8b0 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 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.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 51c841451..157047774 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 @@ 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; @@ -14,7 +15,6 @@ import javax.inject.Singleton; 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/category/CategoryClient.kt b/app/src/main/java/fr/free/nrw/commons/category/CategoryClient.kt index 992c4ed1c..5571e0ea7 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/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index fcfd32974..e910799d0 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 class ContributionController { }, R.string.storage_permission_title, R.string.write_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE); + PermissionUtils.getPERMISSIONS_STORAGE()); } /** @@ -224,7 +224,7 @@ public class ContributionController { () -> 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 a840aa8e1..bffafaef1 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_FAILED; 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.LayoutInflater; 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.annotation.Nullable; 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; @@ -293,7 +289,7 @@ public class ContributionsFragment }); } 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 a9e9ee5c6..03027f287 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.NonNull; 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.notification.NotificationController; 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; @@ -420,7 +414,7 @@ public class MainActivity extends BaseActivity 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/customselector/helper/FolderDeletionHelper.kt b/app/src/main/java/fr/free/nrw/commons/customselector/helper/FolderDeletionHelper.kt new file mode 100644 index 000000000..0ead4c289 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/customselector/helper/FolderDeletionHelper.kt @@ -0,0 +1,249 @@ +package fr.free.nrw.commons.customselector.helper + +import android.content.ContentUris +import android.content.Context +import android.media.MediaScannerConnection +import android.net.Uri +import android.os.Build +import android.provider.MediaStore +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.IntentSenderRequest +import androidx.appcompat.app.AlertDialog +import fr.free.nrw.commons.R +import timber.log.Timber +import java.io.File + +object FolderDeletionHelper { + + /** + * Prompts the user to confirm deletion of a specified folder and, if confirmed, deletes it. + * + * @param context The context used to show the confirmation dialog and manage deletion. + * @param folder The folder to be deleted. + * @param onDeletionComplete Callback invoked with `true` if the folder was + * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request. + * successfully deleted, `false` otherwise. + */ + fun confirmAndDeleteFolder( + context: Context, + folder: File, + trashFolderLauncher: ActivityResultLauncher, + onDeletionComplete: (Boolean) -> Unit) { + val itemCount = countItemsInFolder(context, folder) + val folderPath = folder.absolutePath + + //don't show this dialog on API 30+, it's handled automatically using MediaStore + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + val success = deleteFolderMain(context, folder, trashFolderLauncher) + onDeletionComplete(success) + + } else { + AlertDialog.Builder(context) + .setTitle(context.getString(R.string.custom_selector_confirm_deletion_title)) + .setMessage(context.getString(R.string.custom_selector_confirm_deletion_message, folderPath, itemCount)) + .setPositiveButton(context.getString(R.string.custom_selector_delete)) { _, _ -> + + //proceed with deletion if user confirms + val success = deleteFolderMain(context, folder, trashFolderLauncher) + onDeletionComplete(success) + } + .setNegativeButton(context.getString(R.string.custom_selector_cancel)) { dialog, _ -> + dialog.dismiss() + onDeletionComplete(false) + } + .show() + } + } + + /** + * Deletes the specified folder, handling different Android storage models based on the API + * + * @param context The context used to manage storage operations. + * @param folder The folder to delete. + * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request. + * @return `true` if the folder deletion was successful, `false` otherwise. + */ + private fun deleteFolderMain( + context: Context, + folder: File, + trashFolderLauncher: ActivityResultLauncher): Boolean + { + return when { + //for API 30 and above, use MediaStore + Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> trashFolderContents(context, folder, trashFolderLauncher) + + //for API 29 ('requestLegacyExternalStorage' is set to true in Manifest) + // and below use file system + else -> deleteFolderLegacy(folder) + } + } + + /** + * Moves all contents of a specified folder to the trash on devices running + * Android 11 (API level 30) and above. + * + * @param context The context used to access the content resolver. + * @param folder The folder whose contents are to be moved to the trash. + * @param trashFolderLauncher An ActivityResultLauncher for handling the result of the trash request. + * @return `true` if the trash request was initiated successfully, `false` otherwise. + */ + private fun trashFolderContents( + context: Context, + folder: File, + trashFolderLauncher: ActivityResultLauncher): Boolean + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false + + val contentResolver = context.contentResolver + val folderPath = folder.absolutePath + val urisToTrash = mutableListOf() + + // Use URIs specific to media items + val mediaUris = listOf( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + ) + + for (mediaUri in mediaUris) { + val selection = "${MediaStore.MediaColumns.DATA} LIKE ?" + val selectionArgs = arrayOf("$folderPath/%") + + contentResolver.query(mediaUri, arrayOf(MediaStore.MediaColumns._ID), selection, + selectionArgs, null) + ?.use{ cursor -> + val idColumn = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID) + while (cursor.moveToNext()) { + val id = cursor.getLong(idColumn) + val fileUri = ContentUris.withAppendedId(mediaUri, id) + urisToTrash.add(fileUri) + } + } + } + + //proceed with trashing if we have valid URIs + if (urisToTrash.isNotEmpty()) { + try { + val trashRequest = MediaStore.createTrashRequest(contentResolver, urisToTrash, true) + val intentSenderRequest = IntentSenderRequest.Builder(trashRequest.intentSender).build() + trashFolderLauncher.launch(intentSenderRequest) + return true + } catch (e: SecurityException) { + Timber.tag("DeleteFolder").e(context.getString(R.string.custom_selector_error_trashing_folder_contents, e.message)) + } + } + return false + } + + + /** + * Counts the number of items in a specified folder, including items in subfolders. + * + * @param context The context used to access the content resolver. + * @param folder The folder in which to count items. + * @return The total number of items in the folder. + */ + private fun countItemsInFolder(context: Context, folder: File): Int { + val contentResolver = context.contentResolver + val folderPath = folder.absolutePath + val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + val selection = "${MediaStore.Images.Media.DATA} LIKE ?" + val selectionArgs = arrayOf("$folderPath/%") + + return contentResolver.query( + uri, + arrayOf(MediaStore.Images.Media._ID), + selection, + selectionArgs, + null)?.use { cursor -> + cursor.count + } ?: 0 + } + + + + /** + * Refreshes the MediaStore for a specified folder, updating the system to recognize any changes + * + * @param context The context used to access the MediaScannerConnection. + * @param folder The folder to refresh in the MediaStore. + */ + fun refreshMediaStore(context: Context, folder: File) { + MediaScannerConnection.scanFile( + context, + arrayOf(folder.absolutePath), + null + ) { _, _ -> } + } + + + + /** + * Deletes a specified folder and all of its contents on devices running + * Android 10 (API level 29) and below. + * + * @param folder The `File` object representing the folder to be deleted. + * @return `true` if the folder and all contents were deleted successfully; `false` otherwise. + */ + private fun deleteFolderLegacy(folder: File): Boolean { + return folder.deleteRecursively() + } + + + /** + * Retrieves the absolute path of a folder given its unique identifier (bucket ID). + * + * @param context The context used to access the content resolver. + * @param folderId The unique identifier (bucket ID) of the folder. + * @return The absolute path of the folder as a `String`, or `null` if the folder is not found. + */ + fun getFolderPath(context: Context, folderId: Long): String? { + val projection = arrayOf(MediaStore.Images.Media.DATA) + val selection = "${MediaStore.Images.Media.BUCKET_ID} = ?" + val selectionArgs = arrayOf(folderId.toString()) + + context.contentResolver.query( + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + projection, + selection, + selectionArgs, + null + )?.use { cursor -> + if (cursor.moveToFirst()) { + val fullPath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)) + return File(fullPath).parent + } + } + return null + } + + /** + * Displays an error message to the user and logs it for debugging purposes. + * + * @param context The context used to display the Toast. + * @param message The error message to display and log. + * @param folderName The name of the folder to delete. + */ + fun showError(context: Context, message: String, folderName: String) { + Toast.makeText(context, + context.getString(R.string.custom_selector_folder_deleted_failure, folderName), + Toast.LENGTH_SHORT).show() + Timber.tag("DeleteFolder").e(message) + } + + /** + * Displays a success message to the user. + * + * @param context The context used to display the Toast. + * @param message The success message to display. + * @param folderName The name of the folder to delete. + */ + fun showSuccess(context: Context, message: String, folderName: String) { + Toast.makeText(context, + context.getString(R.string.custom_selector_folder_deleted_success, folderName), + Toast.LENGTH_SHORT).show() + Timber.tag("DeleteFolder").d(message) + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt index 1d9fcd118..50d09361d 100644 --- a/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/CustomSelectorActivity.kt @@ -13,11 +13,26 @@ import android.view.View import android.view.Window import android.widget.Button import android.widget.ImageButton +import android.widget.PopupMenu import android.widget.TextView import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -34,6 +49,7 @@ import fr.free.nrw.commons.customselector.data.MediaReader import fr.free.nrw.commons.customselector.database.NotForUploadStatus import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants +import fr.free.nrw.commons.customselector.helper.FolderDeletionHelper import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.ImageSelectListener import fr.free.nrw.commons.customselector.model.Image @@ -136,10 +152,23 @@ class CustomSelectorActivity : private var showPartialAccessIndicator by mutableStateOf(false) - private val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ result -> + /** + * Show delete button in folder + */ + private var showOverflowMenu = false + + /** + * Waits for confirmation of delete folder + */ + private val startForFolderDeletionResult = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){ + result -> onDeleteFolderResultReceived(result) + } + + private val startForResult = registerForActivityResult(StartActivityForResult()){ result -> onFullScreenDataReceived(result) } + /** * onCreate Activity, sets theme, initialises the view model, setup view. */ @@ -264,6 +293,15 @@ class CustomSelectorActivity : } } + private fun onDeleteFolderResultReceived(result: ActivityResult){ + if (result.resultCode == Activity.RESULT_OK){ + FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName) + navigateToCustomSelector() + } + } + + + /** * Show Custom Selector Welcome Dialog. */ @@ -445,10 +483,97 @@ class CustomSelectorActivity : val limitError: ImageButton = findViewById(R.id.image_limit_error) limitError.visibility = View.INVISIBLE limitError.setOnClickListener { displayUploadLimitWarning() } + + val overflowMenu: ImageButton = findViewById(R.id.menu_overflow) + if(defaultKvStore.getBoolean("displayDeletionButton")) { + overflowMenu.visibility = if (showOverflowMenu) View.VISIBLE else View.INVISIBLE + overflowMenu.setOnClickListener { showPopupMenu(overflowMenu) } + }else{ + overflowMenu.visibility = View.GONE + } + + } + + private fun showPopupMenu(anchorView: View) { + val popupMenu = PopupMenu(this, anchorView) + popupMenu.menuInflater.inflate(R.menu.menu_custom_selector, popupMenu.menu) + + popupMenu.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_delete_folder -> { + deleteFolder() + true + } + else -> false + } + } + popupMenu.show() } /** - * override on folder click, change the toolbar title on folder click. + * Deletes folder based on Android API version. + */ + private fun deleteFolder() { + val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: run { + FolderDeletionHelper.showError(this, "Failed to retrieve folder path", bucketName) + return + } + + val folder = File(folderPath) + if (!folder.exists() || !folder.isDirectory) { + FolderDeletionHelper.showError(this,"Folder not found or is not a directory", bucketName) + return + } + + FolderDeletionHelper.confirmAndDeleteFolder(this, folder, startForFolderDeletionResult) { success -> + if (success) { + //for API 30+, navigation is handled in 'onDeleteFolderResultReceived' + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + FolderDeletionHelper.showSuccess(this, "Folder deleted successfully", bucketName) + navigateToCustomSelector() + } + } else { + FolderDeletionHelper.showError(this, "Failed to delete folder", bucketName) + } + } + } + + + + /** + * Navigates back to the main `FolderFragment`, refreshes the MediaStore, resets UI states, + * and reloads folder data. + */ + private fun navigateToCustomSelector() { + + val folderPath = FolderDeletionHelper.getFolderPath(this, bucketId) ?: "" + val folder = File(folderPath) + + supportFragmentManager.popBackStack(null, + androidx.fragment.app.FragmentManager.POP_BACK_STACK_INCLUSIVE) + + //refresh MediaStore for the deleted folder path to ensure metadata updates + FolderDeletionHelper.refreshMediaStore(this, folder) + + //replace the current fragment with FolderFragment to go back to the main screen + supportFragmentManager.beginTransaction() + .replace(R.id.fragment_container, FolderFragment.newInstance()) + .commitAllowingStateLoss() + + //reset toolbar and flags + isImageFragmentOpen = false + showOverflowMenu = false + setUpToolbar() + changeTitle(getString(R.string.custom_selector_title), 0) + + //fetch updated folder data + fetchData() + } + + + /** + * override on folder click, + * change the toolbar title on folder click, make overflow menu visible */ override fun onFolderClick( folderId: Long, @@ -466,6 +591,11 @@ class CustomSelectorActivity : bucketId = folderId bucketName = folderName isImageFragmentOpen = true + + //show the overflow menu only when a folder is clicked + showOverflowMenu = true + setUpToolbar() + } /** @@ -581,6 +711,10 @@ class CustomSelectorActivity : isImageFragmentOpen = false changeTitle(getString(R.string.custom_selector_title), 0) } + + //hide overflow menu when not in folder + showOverflowMenu = false + setUpToolbar() } /** 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 dbab629ff..3912a4d12 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/customselector/ui/selector/ImageLoader.kt b/app/src/main/java/fr/free/nrw/commons/customselector/ui/selector/ImageLoader.kt index 95c768c1c..ddfcf341e 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 @@ -18,6 +18,7 @@ import fr.free.nrw.commons.utils.CustomSelectorUtils.Companion.checkWhetherFileE import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope import kotlinx.coroutines.launch import java.util.Calendar import java.util.concurrent.TimeUnit @@ -65,7 +66,7 @@ class ImageLoader /** * Coroutine Scope. */ - private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) + private val scope: CoroutineScope = MainScope() /** * Query image and setUp the view. 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 02f7d418e..134ee48d9 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.actions.PageEditClient; 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/delete/ReasonBuilder.java b/app/src/main/java/fr/free/nrw/commons/delete/ReasonBuilder.java index 35d682248..7912375a4 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 @@ package fr.free.nrw.commons.delete; 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/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 1390bd8ef..0d847b649 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 @@ package fr.free.nrw.commons.di; 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/explore/ExploreFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/ExploreFragment.java index 26c8dd82b..d444148d4 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.NonNull; 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/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java index abb27184f..934bff6ec 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 class SearchActivity extends BaseActivity 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 class SearchActivity extends BaseActivity @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/explore/map/ExploreMapFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/map/ExploreMapFragment.java index 52a5571e9..441f46e61 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.LocationChange 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.Location; 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.databinding.FragmentExploreMapBinding; 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.nearby.Place; 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 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment } private void startMapWithoutPermission() { - lastKnownLocation = MapUtils.defaultLatLng; + lastKnownLocation = MapUtils.getDefaultLatLng(); moveCameraToPosition( new GeoPoint(lastKnownLocation.getLatitude(), lastKnownLocation.getLongitude())); presenter.onMapReady(exploreMapController); @@ -331,7 +322,7 @@ public class ExploreMapFragment extends CommonsDaggerSupportFragment !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/filepicker/Constants.java b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java index 4a5823252..97a16acc3 100644 --- a/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java +++ b/app/src/main/java/fr/free/nrw/commons/filepicker/Constants.java @@ -20,4 +20,4 @@ public interface Constants { String COPY_TAKEN_PHOTOS = "fr.free.nrw.commons.copy_taken_photos"; String COPY_PICKED_IMAGES = "fr.free.nrw.commons.copy_picked_images"; } -} \ No newline at end of file +} 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 142d8379c..ed20809ac 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.Utils; 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; @@ -272,6 +271,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements if (!sessionManager.isUserLoggedIn()) { binding.categoryEditButton.setVisibility(GONE); + binding.descriptionEdit.setVisibility(GONE); + binding.depictionsEditButton.setVisibility(GONE); + } else { + binding.categoryEditButton.setVisibility(VISIBLE); + binding.descriptionEdit.setVisibility(VISIBLE); + binding.depictionsEditButton.setVisibility(VISIBLE); } if(applicationKvStore.getBoolean("login_skipped")){ @@ -313,7 +318,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } 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 { @@ -323,7 +328,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements }, R.string.storage_permission_title, R.string.read_storage_permission_rationale, - PermissionUtils.PERMISSIONS_STORAGE + PermissionUtils.getPERMISSIONS_STORAGE() ); } } @@ -400,7 +405,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements } ); binding.progressBarEdit.setVisibility(GONE); - binding.descriptionEdit.setVisibility(VISIBLE); } @Override @@ -678,7 +682,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements // Stick in a filler element. allCategories.add(getString(R.string.detail_panel_cats_none)); } - binding.categoryEditButton.setVisibility(VISIBLE); + if(sessionManager.isUserLoggedIn()) { + binding.categoryEditButton.setVisibility(VISIBLE); + } rebuildCatList(allCategories); } 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 index 8d6b74231..8ed37a293 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/OkHttpJsonApiClient.java @@ -285,7 +285,7 @@ public class OkHttpJsonApiClient { throws Exception { Timber.d("Fetching nearby items at radius %s", radius); - Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null)); + Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); final String wikidataQuery; if (customQuery != null) { wikidataQuery = customQuery; @@ -344,7 +344,7 @@ public class OkHttpJsonApiClient { final boolean shouldQueryForMonuments, final String customQuery) throws Exception { - Timber.d("CUSTOM_SPARQL%s", String.valueOf(customQuery != null)); + Timber.d("CUSTOM_SPARQL: %s", String.valueOf(customQuery != null)); final String wikidataQuery; if (customQuery != null) { 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 0bd8333e3..9ea59488e 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 @@ public class MoreBottomSheetFragment extends BottomSheetDialogFragment { } 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/nearby/fragments/NearbyParentFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/fragments/NearbyParentFragment.java index 6a2e5c3a9..fdbc727bc 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; 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 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment = 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 class NearbyParentFragment extends CommonsDaggerSupportFragment 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 class NearbyParentFragment extends CommonsDaggerSupportFragment .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 410aeb9f4..00a491e68 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.nearby.CheckBoxTriStates.UNKNOWN; 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.CheckBoxTriStates; 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/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java deleted file mode 100644 index 572dd0317..000000000 --- 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); - }); - compositeDisposable.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); - compositeDisposable.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 000000000..1d87a8f82 --- /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 41d7d4883..637443ecf 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 de1f372d2..000000000 --- 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 000000000..870d658cb --- /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 b63d3a4c1..000000000 --- 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 000000000..101a8fccc --- /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 ffee5eac2..000000000 --- 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 000000000..928651b9a --- /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 fb9ae7e99..000000000 --- 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 000000000..9034b3c59 --- /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 + } + } +} 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 c6d09fdc6..60a0f47a1 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 @@ public class ProfileActivity extends BaseActivity { @Override public void onDestroy() { super.onDestroy(); - compositeDisposable.clear(); + getCompositeDisposable().clear(); } /** 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 861040fcf..7b23db2cd 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 f44b7eb6d..ef6a323b2 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 class AchievementsFragment extends CommonsDaggerSupportFragment { // 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { * @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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { }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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { // 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { 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 @@ public class AchievementsFragment extends CommonsDaggerSupportFragment { } 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 4784103fd..2a336d349 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, ) 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 8c087b17b..000000000 --- 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 000000000..a243c2637 --- /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 201c5bfc6..000000000 --- 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 000000000..ec74ecf6f --- /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 a7b2c94ef..000000000 --- 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 000000000..3cb4f52a6 --- /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 ec6d1070d..000000000 --- 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 000000000..1d4821ee3 --- /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 79756871d..000000000 --- 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 000000000..8afdf94c5 --- /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 + } + } +} 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 de94c4b09..000000000 --- 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 000000000..facc4384f --- /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 cbb8c8a1c..000000000 --- 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 000000000..a4a06185b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/recentlanguages/RecentLanguagesDao.kt @@ -0,0 +1,215 @@ +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 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 + ) + + const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME" + + 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 de0154947..000000000 --- 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 000000000..377953254 --- /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 timber.log.Timber +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton + +/** + * 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/review/ReviewActivity.java b/app/src/main/java/fr/free/nrw/commons/review/ReviewActivity.java deleted file mode 100644 index 40d743a19..000000000 --- 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 000000000..44b0f9bc1 --- /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 e3d5b2256..000000000 --- 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 000000000..62652bd5b --- /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 c3e8c90a8..1dc9b6ae8 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 071111b15..000000000 --- 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 000000000..473c143c7 --- /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 8a77c11ed..17296a5c8 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 7e0cd0ee3..000000000 --- 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 000000000..691c61f56 --- /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 16b55c6e9..000000000 --- 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 000000000..9bbe14e65 --- /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 95740aac0..000000000 --- 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 000000000..39de49189 --- /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 + } +} 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 334214347..000000000 --- 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 000000000..13e8efb57 --- /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 ff5024b32..000000000 --- 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 000000000..da79244bc --- /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 5e631425b..000000000 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ /dev/null @@ -1,561 +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; - -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 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; - } - }); - - 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.PERMISSIONS_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.PERMISSIONS_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 000000000..b55ac6009 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -0,0 +1,558 @@ +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() + } + + 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") + } + } + } + + /** + * 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() + } + +} 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 95ec00dc6..000000000 --- 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 000000000..d2d936460 --- /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 0b82baf64..000000000 --- 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 000000000..56f795485 --- /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 21af5ee78..000000000 --- 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 000000000..48433136f --- /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 f36219040..000000000 --- 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 000000000..5d4bc2b46 --- /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 + } +} 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 35906c3fb..ed65b05df 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.recyclerview.widget.LinearLayoutManager; 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 @@ public class UploadActivity extends BaseActivity implements UploadContract.View, 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 class UploadActivity extends BaseActivity implements UploadContract.View, }, 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/upload/UploadMediaDetailAdapter.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadMediaDetailAdapter.java index 6fc8b3266..a1a639a59 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.ListView; 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/categories/CategoriesPresenter.kt b/app/src/main/java/fr/free/nrw/commons/upload/categories/CategoriesPresenter.kt index 1822df830..712f6fc3e 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 3beedd9d5..fa3eb354e 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/mediaDetails/UploadMediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/mediaDetails/UploadMediaDetailFragment.java index 5581cfeb1..fb836445a 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.UploadBaseFragment; 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 class UploadMediaDetailFragment extends UploadBaseFragment implements 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/upload/structure/depictions/DepictModel.kt b/app/src/main/java/fr/free/nrw/commons/upload/structure/depictions/DepictModel.kt index 7242b8eed..9337cb8b5 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/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt b/app/src/main/java/fr/free/nrw/commons/upload/worker/UploadWorker.kt index 144c503bb..00cd29a6d 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!! } 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 d5188027d..000000000 --- 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 000000000..dd06452f9 --- /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 4806585dc..000000000 --- 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 000000000..899daaf6b --- /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 39ddca683..000000000 --- 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 000000000..c076e19ce --- /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 1d2a8fdf7..000000000 --- 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 000000000..bc33a1ede --- /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 5e01cc606..000000000 --- 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 000000000..05d71c7e1 --- /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 889b31f2d..000000000 --- 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 000000000..981b19355 --- /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 a01ff9251..000000000 --- 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 000000000..4cdeecda2 --- /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/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java deleted file mode 100644 index 99155a5e3..000000000 --- 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 000000000..78a877600 --- /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 634a73ad2..000000000 --- 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 000000000..8393dc652 --- /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 73bd5c02b..000000000 --- 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 000000000..5ef21a735 --- /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 76c52527b..000000000 --- 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 000000000..71e6697f7 --- /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 0ca61a1d9..000000000 --- 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 000000000..48cf1a020 --- /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 01a885538..000000000 --- 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 000000000..2df42270e --- /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 d3b5bd0e2..000000000 --- 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 000000000..adc3a5d90 --- /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 8eb875bb5..000000000 --- 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 000000000..93cdabbfc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/utils/MediaDataExtractorUtil.kt @@ -0,0 +1,25 @@ +package fr.free.nrw.commons.utils + +object MediaDataExtractorUtil { + + /** + * Extracts a list of categories from | separated category string + * + * @param source + * @return + */ + @JvmStatic + fun extractCategoriesFromList(source: String?): List { + if (source.isNullOrBlank()) { + 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 bc6e6883f..000000000 --- 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 000000000..61b95a413 --- /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 ce64cb031..000000000 --- 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 000000000..98fde9ef7 --- /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 828ef2338..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/PermissionUtils.java +++ /dev/null @@ -1,219 +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 }; - } - 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 000000000..305388fab --- /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 f1022a041..000000000 --- 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 000000000..907420f21 --- /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 314467972..000000000 --- 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 000000000..d9f813ae0 --- /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 a5bb6038e..000000000 --- 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 000000000..b3c58d8b2 --- /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 7ea7ef467..000000000 --- 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 000000000..5a8261c24 --- /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 aa60a7aa8..000000000 --- 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 000000000..f4b1f2625 --- /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 acb6afbaa..000000000 --- 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 000000000..9ff069ebc --- /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 1272dc4f1..000000000 --- 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 000000000..64970ecf6 --- /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 2721ef98d..000000000 --- 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 000000000..b5ead3041 --- /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) + } +} 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 5c6dde5fd..000000000 --- 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 000000000..b86455243 --- /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 273452078..000000000 --- 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 000000000..ab6a45b85 --- /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 e2dd8d680..000000000 --- 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 000000000..f9f598b3e --- /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 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 2b18669a4..2d1dbdf28 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.JsonElement; 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; diff --git a/app/src/main/res/layout/activity_custom_selector.xml b/app/src/main/res/layout/activity_custom_selector.xml index 02c864422..44e494cb9 100644 --- a/app/src/main/res/layout/activity_custom_selector.xml +++ b/app/src/main/res/layout/activity_custom_selector.xml @@ -11,7 +11,7 @@ android:id="@+id/partial_access_indicator" android:layout_width="match_parent" android:layout_height="0dp" - app:layout_constraintTop_toBottomOf="@id/toolbar_layout"/> + app:layout_constraintTop_toBottomOf="@id/toolbar_layout" /> + + + + + app:layout_constraintStart_toEndOf="@id/image_limit_error" + app:srcCompat="@drawable/ic_overflow" /> + \ 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 000000000..fc432439e --- /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-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 22f382f57..9ff1b19b4 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 57ba77cc9..1da8b3101 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 e4029af9b..70a01949f 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 b0fda6990..acb7cd122 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -125,7 +125,7 @@ يجري الدخول الرجاء الانتظار… تحديث التسميات التوضيحية والأوصاف - يرجى الانتظار… + يرجى الانتظار... نجاح تسجيل الدخول! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. @@ -171,6 +171,7 @@ تصنيفات البحث (.البحث عن العناصر التي تصورها وسائطك (جبل ،تاج محل، إلخ حفظ + القائمة الزائدة أنعش القائمة (لا يوجد تحميلات حتى الآن) @@ -527,10 +528,11 @@ ليس لديك أي إشعارات غير مقروءة ليس لديك أي إشعاراتٍ غير مقروءة مشاركة السجلات باستخدام + تحقق من صندوق بريدك الإلكتروني عرض المقروءة عرض غير المقروءة حدث خطأ أثناء التقاط الصور - الرجاء الانتظار… + الرجاء الانتظار... الصور المختارة هي صور من مصورين ورسامين ذوي مهارات عالية اختارها مجتمع ويكيميديا ​​كومنز كبعض الأفضل جودة على الموقع. الصور المرفوعة عبر الأماكن القريبة هي الصور المرفوعة عن طريق اكتشاف الأماكن على الخريطة. تتيح هذه الميزة للمحررين إرسال إشعار شكر للمستخدمين الذين يقومون بتعديلات مفيدة - باستخدام رابط شكر صغير في صفحة التاريخ أو صفحة الفرق. @@ -552,7 +554,7 @@ رفض الوصول إلى موقع الوسائط قد لا نتمكن من الحصول تلقائيًا على بيانات الموقع من الصور التي تقوم برفعها. يرجى إضافة الموقع المناسب لكل صورة قبل الإرسال ارفع الصور لويكيميديا ​​كومنز مباشرة من هاتفك. قم بتنزيل تطبيق كومنز الآن: %1$s - مشاركة التطبيق عبر… + مشاركة التطبيق عبر... معلومات الصورة لم يتم العثور على تصنيفات لم يتم العثور على الصور @@ -690,12 +692,13 @@ مجاور مستخدَم ترتيبي + &#169; <a href=\"https://www.openstreetmap.org/copyright\">خريطة الشارع المفتوحة</a> وضع الاتصال المحدود مُمَكَّن! وضع الاتصال المحدود مُعطل. سيجري استئناف التحميلات المعلقة الآن. وضع الاتصال المحدود صور عالية الجودة الصور عالية الجودة هي رسوم بيانية أو صور فوتوغرافية تفي بمعايير جودة معينة (والتي تكون في الغالب ذات طبيعة فنية) وذات قيمة لمشروعات ويكيميديا - جاري استئناف التحميل … + جاري استئناف التحميل ... جاري إيقاف التحميل مؤقتًا .. الغاء التحميل إلغاء الرفع @@ -739,6 +742,7 @@ رفض الحد الأقصى: %1$d خطأ: تجاوز حد التحميل + دبليو إل إم سيتم إدخال هذه الصورة في مسابقة Wiki Loves Monuments عرض الآثار إنه شهر Wiki Loves Monuments! @@ -788,19 +792,75 @@ تم تحديد الصورة تم وضع علامة على الصورة على أنها ليست للتحميل تقرير + تعيين الخلفية البيضاء + تعيين خلفية سوداء تبليغ عن عنف أخطر عن هذا المستخدم الإبلاغ عن هذا المحتوى طلب منع هذا المستخدم مرحبًا بك في وضع التحديد بملء الشاشة استخدم إصبعين للتكبير والتصغير. - مرر سريعًا وطويلًا لتنفيذ هذه الإجراءات:! N! - يسار / يمين: انتقل إلى السابق / التالي! N! - لأعلى: حدد! N! - أسفل: وضع علامة على أنه ليس للتحميل. + مرر سريعًا وطويلًا لأداء هذه الإجراءات: \n- يسار/يمين: الانتقال إلى السابق/التالي \n- أعلى: تحديد\n- أسفل: وضع علامة على عدم التحميل. + لإعداد صورتك الرمزية في قائمة المتصدرين، اضغط على \"تعيين كصورة رمزية\" في قائمة النقاط الثلاث لأي صورة. + الإحداثيات ليست إحداثيات دقيقة، لكن الشخص الذي قام بتحميل هذه الصورة يعتقد أنها قريبة بما فيه الكفاية. رُفض إذن التخزين تعذر مشاركة هذا العنصر الإذن مطلوب لهذه الوظيفة تعلم كيفية كتابة وصف مفيد + تعلم كيفية كتابة تعليق مفيد + شاهد إنجازاتك + تعديل الصورة + تعديل الموقع + تم تحديث الموقع! + إزالة الموقع إزالة تحذير الموقع + يجعل تحديد الموقع الصور أكثر فائدة وسهولة في العثور عليها. هل ترغب حقًا في إزالة تحديد الموقع من هذه الصورة؟ + تمت إزالة الموقع! اشكر المؤلف حدث خطأ أثناء إرسال الشكر للمؤلف. + لقد انتهت صلاحية تسجيل الدخول الخاص بك. يرجى تسجيل الدخول مرة أخرى. + لا يوجد تطبيق متاح لفتح ملفات GPX + تم حفظ الملف بنجاح + هل تريد فتح ملف GPX؟ + هل تريد فتح ملف KML؟ + فشل في حفظ ملف KML. + فشل في حفظ ملف GPX. + حفظ ملف KML + حفظ ملف GPX + + لا صور تم اختيارها + %d صورة تم اختيارها + صورتان تم اختيارهما + صور قليلة تم اختيارها + صور كثيرة تم اختيارها + %d صور تم اختيارها + + يرجى تذكر أن جميع الصور في التحميل المتعدد تحصل على نفس الفئات والأوصاف. إذا لم تتشارك الصور في الأوصاف والفئات، فيرجى إجراء عدة عمليات تحميل منفصلة. + ملاحظة حول التحميلات المتعددة + الإبلاغ عن مشكلة حول هذا العنصر إلى Wikidata + الرجاء إدخال بعض التعليقات + نقاش + اكتب شيئًا عن العنصر \'%1$s\'. سيكون مرئيًا للعامة. + \'%1$s\' لم يعد موجودًا، ولا يمكن التقاط صورة له أبدًا. + \'%1$s\' موجود في مكان مختلف. يرجى تحديد المكان الصحيح أدناه، وإذا أمكن، اكتب خط العرض وخط الطول الصحيحين. + مشكلة أو معلومات أخرى (يرجى التوضيح أدناه). + سيتم نشر تعليقاتك على صفحة الويكي التالية: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + هل أنت متأكد أنك تريد إلغاء كافة التحميلات؟ + إلغاء كافة التحميلات... + المرفوعات + قيد الانتظار + فشل تعذر تحميل بيانات المكان + حذف المجلد + تأكيد الحذف + هل أنت متأكد أنك تريد حذف المجلد %1$s الذي يحتوي على %2$d عنصرا؟ + حذف + إلغاء + تم حذف المجلد %1$s بنجاح + فشل حذف المجلد %1$s + خطأ في نقل محتويات المجلد إلى سلة المهملات: %1$s + فشل استرداد مسار المجلد لمعرف الدلو: %1$d + هذا المكان ليس له صورة بعد، اذهب والتقط واحدة! + هذا المكان لديه صورة بالفعل. + الآن التحقق ما إذا كان هذا المكان لديه صورة. diff --git a/app/src/main/res/values-as/strings.xml b/app/src/main/res/values-as/strings.xml index 63aa12b96..960b55bda 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 34212aebb..df61ed061 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 d2ea468ad..d97d5ed20 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 @@ -104,7 +105,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ə @@ -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-b+roa+tara/strings.xml b/app/src/main/res/values-b+roa+tara/strings.xml index 8e7764323..4fa660ef8 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 b8b602d0d..cd1cb09e8 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 4c33b396f..0fc68329f 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 1273eaf0d..b24bc0022 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 cb19d6e39..6ee931542 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 51a8ec1ba..2cf4aba5b 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 51502c264..2d156c199 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 9537c45e6..5d86b1fcf 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,11 +51,15 @@ 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ñ + + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + %1$d enporzhiadur + Ergerzhout Neuz Hollek @@ -79,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 @@ -185,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 @@ -259,6 +261,7 @@ Commons Ho priziadur FAG + Sturlevr an implijer Lezel an tutorial a-gostez Kenrouedad dihegerz Fazi war enklask kemennoù @@ -299,6 +302,7 @@ Respont fall Rannañ an arloadoù C\'hwelañ + Skeudenn ebet en takad-mañ Klask nevez ebet Istor klask diverket Dilemel @@ -314,6 +318,7 @@ Skeudennoù implijet Ur fazi zo bet ! Implijit un anv aozer personelaet + Anv aozer personelaet Degasadennoù Nepell Kemennoù @@ -335,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 @@ -358,6 +365,7 @@ Lec\'hiadur Doare kamera Meziant + Rannañ an arload gant... Titouroù ar skeudenn Rummad ebet kavet Deskrivadur ebet kavet @@ -371,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ù @@ -406,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ù @@ -422,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 @@ -438,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-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index d178ff507..91860b1e1 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 51330cb9d..0c15e58c3 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 e25b83e25..e13e8c040 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 fb4ee05ef..5ec23c385 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,48 +29,62 @@ 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 soubory se nahrávají - %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ů - + (%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ů + 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í - %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ávání - %1$d nahrávání + %1$d nahrání + %1$d nahrá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 @@ -80,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 @@ -88,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 @@ -501,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-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 7cdfb1382..623a48c8c 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 50df9b6d8..8c4b4a652 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 80a82afb5..2b5dcc914 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) @@ -507,7 +508,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 +643,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,11 +785,20 @@ 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 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-de/strings.xml b/app/src/main/res/values-de/strings.xml index 404304021..c19772f4d 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,11 +513,12 @@ 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 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 +613,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 @@ -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, der %2$d Datenobjekte enthält? + 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-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index ebb3cfbe4..840b0198d 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 e4b597fb1..e3675ef0a 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 323c823b2..69673afbe 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 218953470..4e90f6864 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 3dd463b34..ff75cbc7f 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 4cdd2b87a..841160581 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 26328a3e2..312ebc84c 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 995e4041b..ecb27df91 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. @@ -168,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) @@ -192,7 +186,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 +226,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 +522,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 +544,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 +571,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 +579,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 +621,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 +679,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 +810,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 +823,21 @@ 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 + 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-gcr/strings.xml b/app/src/main/res/values-gcr/strings.xml index 4659eecf1..b0ec66423 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 e11716a51..1740c1890 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 237583853..55a378eaf 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,8 +350,11 @@ रद्द करें वार्ता क्या आप वाकई सभी अपलोड रद्द करना चाहते हैं? - सभी अपलोड रद्द किये जा रहे हैं… + सभी अपलोड रद्द किये जा रहे हैं... अपलोड लंबित विफल हुआ + फ़ोल्डर हटाएँ + हटाएँ + रद्द करें diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 414f0dd40..d2d731c39 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 eb3438674..d4e6c0f98 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -374,7 +374,7 @@ A helymeghatározás nélkül nem használható ez a funkció. Ne kérdezd meg többször Helymeghatározási engedély - Valami hiba történt, nem sikerült az eredményeid betöltése + Valami hiba történt, nem sikerült az eredményeid betöltése Kampányok megjelenítése Folyamatban lévő kampányok megjelenítése Engedélyezd az alkalmazás számára a helyszín lekérését, ha a kamera nem rögzíti azt! Egyes eszközök kamerái nem rögzítik a helyszínt. Közreműködésed hasznosabb, ha ilyen esetekben hagyod, hogy az alkalmazás lekérje és hozzárendelje a helyszínt. Ezt bármikor módosíthatod a Beállításokban @@ -385,7 +385,7 @@ Az alkalmazás helymeghatározási engedély hiányában nem rögzíti a helyszínt a felvételekkel együtt Az alkalmazás nem rögzít helyszínt a felvételekkel együtt, mivel a GPS ki van kapcsolva Többé nem lesznek láthatók a kampányok. Ha akarod, visszakapcsolható a Beállításoknál. - Ehhez a funkcióhoz hálózati kapcsolat szükséges, kérlek ellenőrizd az internetbeállításaidat. + Ehhez a funkcióhoz hálózati kapcsolat szükséges. Kérlek ellenőrizd az internetbeállításaidat! Hiba történt a kép feltöltése során. Próbáld meg újra! Szerkesztő token beszerzése Kategóriaellenőrző sablon felhelyezése @@ -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 @@ -454,9 +454,9 @@ Törlésre jelölve: %1$s. Sikertelen Nem sikerült a törlés kérése. - Egy szelfi - Homályos - Nonszensz + egy szelfi, amely egyetlen cikkben sem szerepel + teljesen homályos + nonszensz, abszolút használhatatlan bármely cikkben is Sajtófotó Random fénykép az Internetről Logó @@ -472,9 +472,9 @@ Fénykép szükséges Hely típusa: Híd, múzeum, szálloda, stb. - A belépés nem sikerült, kérj új jelszót. + 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 8fff554e3..219fa4521 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 994b1c3d3..9a9f7d70e 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 @@ -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-is/strings.xml b/app/src/main/res/values-is/strings.xml index ac64fbf2c..417652953 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 e9aa8934e..538f6c884 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,9 +758,13 @@ Sessione scaduta. Accedi nuovamente. %d immagine selezionata - %d immagini selezionate %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-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 4b8c51f6c..be45099a9 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 @@ שכחת את הסיסמה? רישום כניסה לחשבון - נא להמתין… + נא להמתין... עדכון כיתובים ותיאורים - נא להמתין… + נא להמתין... הכניסה הצליחה! הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. @@ -107,7 +114,7 @@ ההעלאה התחילה! ההעלאות בתור (מופעל מצב חיבור מוגבל) הקובץ %1$s הועלה! - ללחוץ כאן כדי לצפות בהעלאה שלך + נא ללחוץ כאן כדי לצפות בהעלאה שלך העלאת קובץ: %s מתבצעת העלאת %1$s העלאת %1$s מסתיימת @@ -116,7 +123,7 @@ לחץ כדי להציג יש לגעת כדי לראות ההעלאות האחרונות שלי - בתור + הוכנסה בתור נכשלה %1$d%% הושלמו העלאה @@ -142,6 +149,7 @@ חיפוש קטגוריות חפשו פריטים שהמדיה שלך מציגה (הר, טאג\' מהאל, וכו\') שמירה + תפריט גלישה רענון רשימה (לא הועלה עדיין שום דבר) @@ -206,7 +214,7 @@ כן! מידע נוסף קטגוריות - בטעינה… + בטעינה... לא נבחר דבר אין כיתוב אין תיאור @@ -498,10 +506,11 @@ אין לך התראות שלא נקראו אין לך התראות שנקראו שיתוף יומנים בעזרת + לבדוק את תיבת הדוא״ל הנכנס שלך הצגת התראות שנקראו הצגת התראות שלא נקראו אירעה שגיאה בעת בחירת תמונות - נא להמתין… + נא להמתין... תמונות מובילות הן תמונות של צלמים ומאיירים מיומנים אותם בחרה קהילת ויקישיתוף בזכות איכות התוצר שהם תורמים לאתר. תמונות שהועלו דרך מקומות בסביבה הן התמונות שנשלחות על ידי גילוי מקומות במפה. תכונה זו מאפשרת לעורכים לשלוח מסרי תודה למשתמשים שביצעו עריכות מועילות - על ידי שימוש בקישור תודה בדף ההיסטוריה או בדף ההבדלים. @@ -523,7 +532,7 @@ הגישה למקום המדיה נדחתה ייתכן שלא נוכל לאתר את נתוני המקום מתמונות שהעלית. נא להוסיף את המקום המתאים לכל תמונה בטרם הגשתה כדי להעלות תמונות לוויקינתונים של ויקימדיה ישר מהטלפון שלך. אתם מוזמנים להוריד את היישום של ויקינתונים עכשיו: %1$s - שיתוף היישום דרך… + שיתוף היישום דרך... פרטי תמונה לא נמצאו קטגוריות לא נמצאו מוצגים @@ -551,6 +560,7 @@ נוספה קטגוריה. נוספו %1$s קטגוריות. + נוספו %1$s קטגוריות. נוספו %1$s קטגוריות. לא ניתן להוסיף קטגוריות. @@ -560,6 +570,7 @@ נוסף מוצג %1$s נוספו המוצגים %1$s + נוספו המוצגים %1$s נוספו המוצגים %1$s לא היה אפשר להוסיף מוצגים. @@ -602,7 +613,7 @@ נוסף לסימניות משהו השתבש. לא היה אפשר להגדיר את הטפט להגדיר בתור טפט - הגדרת טפט. נא להמתין… + הגדרת טפט. נא להמתין... מערכת מעקב כהה בהירה @@ -662,9 +673,9 @@ תמונות איכות הן תרשימים או תמונות שעומדות בתקני איכות מסוימים (שמטבעם בעיקר טכניים) והן בעלות ערך למיזמי ויקימדיה ההעלאה ממשיכה… ההעלאה מושהית… - ביטול ההעלאה… + ביטול ההעלאה... ביטול ההעלאה - הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות ותמשכנה לאחר השבתת המצב הזה. + הפעלת מצב חיבור מוגבל. כל ההעלאות מושהות והן תימשכנה לאחר השבתת המצב הזה. מצב חיבור מוגבל פעיל. נא לכתוב תיאור קצר שמסביר מה מופיע בתמונה. בתיאור, כדאי לכתוב מה הופך את התמונה הזאת למעניינת, טיפוסית או נדירה ולהסביר את ההקשר, בין אם גלוי או סמוי. יש להשתמש במינוח מדויק ככל הניתן. נא למצוא ולבחור את כל העקרונות שהתמונה הזאת מתארת. נא לשמור על דיוק מרבי. אם התמונה מתארת מגוון פריטים, נא לבחור אותם בגבולות הסביר. לא לבחור תגיות גנריות אם יש תגיות יותר נקודתית זמינות. @@ -792,6 +803,7 @@ נבחרה תמונה אחת נבחרו שתי תמונות + נבחרו %d תמונות נבחרו %d תמונות נא לזכור שכשמועלות כמה תמונות, כולן מקבלות את אותן הקטגוריות והמוצגים. אם התמונות אינן חולקות מוצגים וקטגוריות, נא לעשות כמה העלאות נפרדות. @@ -805,9 +817,21 @@ בעיה אחרת או מידע אחר (נא להסביר הלאה). המשוב שלך מתפרסם בדף הוויקי הבא: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> האם ברצונך באמת לבטל את כל ההעלאות? - ביטול כל ההעלאות… + ביטול כל ההעלאות... העלאות ממתינות נכשלו לא היה אפשר לטעון את נתוני המקום + מחיקת תיקייה + אישור מחיקה + למחוק את התיקייה %1$s על כל %2$d פריטיה? + מחיקה + ביטול + התיקייה %1$s נמחקה + מחיקת התיקייה %1$s נכשלה + שגיאה בהעברת תוכן התיקייה לאשפה: %1$s + נכשל אחזור נתיב התיקייה למזהה ההקבצה: %1$d + אין עדיין תמונה למקום הזה, אפשר פשוט לצלם אחת! + למקום הזה כבר יש תמונה. + עכשיו מתבצעת בדיקה האם למקום הזה יש תמונה. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f60bb30dd..f20b986f8 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 eb90e4a23..40eb01629 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 3703d373f..1eaa3594c 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 @@ 로그인 중 기다려 주세요… 캡션 및 설명를 업데이트하는 중 - 기다려 주십시오… + 기다려 주십시오... 로그인 성공! 로그인 실패! 파일을 찾을 수 없습니다. 다른 파일을 사용해 주십시오. @@ -123,6 +129,7 @@ 분류 검색 미디어가 서술한 항목을 검색하세요. (산, 타지마할 등) 저장 + 오버플로 메뉴 새로 고침 목록 (아직 올린 항목이 없음) @@ -278,6 +285,7 @@ 위키텍스트를 클립보드에 복사했습니다 주변이 제대로 작동되지 않을 수 있습니다. 위치를 사용할 수 없습니다. 주변 장소의 목록을 표시하기 위한 권한이 필요합니다. + 주변 장소의 이미지 목록을 표시하기 위한 권한이 필요합니다 방향 위키데이터 위키백과 @@ -433,6 +441,7 @@ 완료 감사 표현 보내기: 성공 감사 표현 보내기: 실패 + 이것이 저작권 규정을 준수하고 있습니까? 알맞게 분류됐습니까? 기여자에게 감사를 표하시겠습니까? 앗, 분류가 달리지 않은 것 같습니다! @@ -447,10 +456,11 @@ 이미지가 올려지지 않음 읽지 않은 알림이 없습니다 읽은 알림이 없습니다 + 이메일의 받은 편지함을 확인하세요 읽은 항목 보기 읽지 않은 항목 보기 이미지 선택 도중 오류가 발생했습니다 - 기다려 주십시오… + 기다려 주십시오... 다음 미디어로 복사 복사했습니다 공용에 업로드할 좋은 이미지의 예 @@ -465,7 +475,7 @@ 렌즈 모델 일련 번호 소프트웨어 - 앱 공유… + 앱 공유... 이미지 정보 분류가 없습니다 서술이 발견되지 않았습니다 @@ -493,6 +503,7 @@ 성공 설명이 추가되었습니다. 캡션이 추가되었습니다. + 좌표를 추가하지 못했습니다. 설명을 추가하지 못했습니다. 캡션을 추가하지 못했습니다. 이미지 좌표가 업데이트되지 않았습니다 @@ -523,7 +534,7 @@ 북마크에 추가됨 무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다 배경화면으로 설정 - 배경화면을 설정 중입니다. 기다려 주십시오… + 배경화면을 설정 중입니다. 기다려 주십시오... 어두운 밝은 위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요 @@ -543,6 +554,7 @@ 일시 정지 계속하기 일시 중단됨 + 더 보기 책갈피 리더보드 순위: @@ -638,6 +650,9 @@ 전체 화면 선택 모드에 오신 것을 환영합니다 두 손가락으로 확대 / 축소하세요. 다음 방향으로 길고 재빠르게 넘겨보세요. \n- 왼쪽/오른쪽: 이전/다음으로 이동 \n- 위쪽: 선택\n- 아래쪽: 비업로드용으로 표시 + 스토리지 접근이 거부됨 + 이 항목을 공유할 수 없습니다 + 기능에 대한 권한이 필요합니다 유용한 설명을 추가하는 법 알아보기 유용한 캡션을 추가하는 법 알아보기 업적 보기 @@ -651,6 +666,7 @@ 작성자에게 감사 표시하기 작성자에게 감사를 표하던 도중에 오류가 발생하였습니다. 로그인 세션 만료. 다시 로그인해 주십시오. + GPX 파일을 열 수 있는 응용 프로그램이 없습니다 파일이 성공적으로 저장되었습니다 GPX 파일을 여시겠습니까? KML 파일을 여시겠습니까? @@ -658,8 +674,28 @@ GPX 파일을 저장하지 못했습니다. KML 파일을 저장 중 GPX 파일을 저장 중 + + %d개 이미지 선택됨 + 다중 업로드에 대한 참고사항 이 항목에 관한 문제를 위키데이터에 보고하기 + 의견을 입력해 주십시오 토론 기타 문제 또는 정보 (아래에 설명해 주십시오) + 모든 업로드를 취소하는 중... + 업로드 + 보류 중 + 실패 + 장소 데이터를 불러오지 못했습니다 + 폴더 삭제 + 삭제 확인 + 항목 %2$d개를 포함하는 %1$s 폴더를 삭제하시겠습니까? + 삭제 + 취소 + %1$s 폴더를 성공적으로 삭제했습니다 + %1$s 폴더를 삭제하지 못했습니다 + 버킷 ID의 폴더 경로를 검색하지 못했습니다: %1$d + 이 장소에 아직 사진이 없습니다. 사진을 찍어보세요! + 이 장소에 이미 사진이 있습니다. + 지금 이 장소에 사진이 있는지 확인 중입니다. diff --git a/app/src/main/res/values-krc/strings.xml b/app/src/main/res/values-krc/strings.xml index be63e9db5..13de1bb12 100644 --- a/app/src/main/res/values-krc/strings.xml +++ b/app/src/main/res/values-krc/strings.xml @@ -119,6 +119,7 @@ Категорияланы изле Медиагъызда суратланнган элементлени излегиз (тау, Тадж Махал э. а. к.) Сакъландыр + Къошакъ меню Джангырт Тизме (Алкъын джюклеуле джокъдула) @@ -143,7 +144,7 @@ Оюмунгу билдир (эл. почта бла) Почта клиент къурулмагъанды Кёб болмай хайырланнган категорияла - Биринчи синхронизацияны сакълаб турады… + Биринчи синхронизацияны сакълаб турады... Алкъын джюкленнген фотосуратыгъыз джокъду. Джангыдан сына Ызына ал @@ -227,7 +228,7 @@ Ызына ал Ач Джаб - Баш бет + Тамал бет Джюкле Джуўукъда Юсюнден @@ -473,6 +474,7 @@ Окъулмагъана хапарландырыуугъуз джокъду Окъулмагъан хапарландырыуугъуз барды Логларыгъызны хайырланыб юлюшлегиз + Электрон почтагъызны тинтигиз Окъулгъанны кёргюз Окъулмагъанланы кёргюз Суратла сайланнган заманда халат болду. @@ -498,7 +500,7 @@ Медиа локациягъа джетишиу уналмады Джюклеген суратладан локация билгилени автомат халда алмазгъа боллукъбуз. Тилейбиз, джибериуден алгъа хар сурат ючюн келишген локацияны къошугъуз Фотосуратланы телефонугъуздан туура Викигёзеннге джюклегиз. Гёзен Къошакъны энди эндиригиз: %1$s - Къошакъны буну бла юлюшле… + Къошакъны буну бла юлюшле... Сурат Информация Категорияла табылмадыла Танытыула табылмадыла @@ -575,7 +577,7 @@ Китаб белгилеге къошулду Не эсе да терс кетди. Къабыргъа къагъыт къурулалмады Къабыргъа къагъыт эт - Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз… + Къабыргъа Къагъыт къурула турады. Тилейбиз сакълагъыз... Системаны джарашдыр Къарангы Джарыкъ @@ -633,9 +635,9 @@ Чекленнген Байланыу Режим Агъачлары Мийик Суратла Агъачлы суратла, белгили агъач стандартларына (асламысыны техника халы болады) келишген эмда Викимедиа проектле ючюн багъалы болгъан диаграммала неда фотосуратладыла - Джюклениу андан ары бардырылады… - Джюклениу туракъланады… - Джюклениу ызына алынады… + Джюклениу андан ары бардырылады... + Джюклениу туракъланады... + Джюклениу ызына алынады... Джюклеуню Ызына Ал Чекли байланыу режимни джандырдыгъыз. Бютеу джюклениуле туракълатыллыкъдыла эмда бу режимни джукълатсагъыз, тохтагъан джерден башларыкъдыла. Чекленнген байланыу режим джандырылгъанды. @@ -686,7 +688,7 @@ Джууукъдагъы картала тюз ишлер ючюн ТЕЛЕФОННУ БОЛУМУн окъургъа амал болургъа кереклиди Хайырланыучуну къошумлары: %s Хайырланыучуну джетишимлери: %s - Хайырланыучу бетни кёргюз + Хайырланыучу профильни кёр Танытыуланы тюзет Категорияланы тюзет Кенгленнген Сайлаула @@ -774,4 +776,22 @@ \'%1$s\' - башха джерди. Тилейбиз, тюз джерни энишгерекде белгилегиз, эмда мадар бар эсе, тюз кенглик бла узунлукъну джазыгъыз. Башха проблема неда информация (тилейбиз, энишгерекде ангылатыгъыз). Сизни кери оюмугъуз тюбюндеги вики бетге джиберилликди: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> + Бютеу джюклеулени тохтатыргъа излегенигизге ишексизмисиз? + Бютеу джюклеулени тохтатыу... + Джюклеуле + Сакълауда + Джетишимсиз + Джерни юсюнден билгилени джюклеялмады + Папканы кетер + Кетериуню мюкюл эт + %2$d элементи болгъан %1$s папканы кетерирге излегенинге ишексизмисе? + Кетер + Ызына ал + %1$s папка джетишимли кетерилгенди + %1$s папканы кетериу джетишимисизди + Папканы ичиндегисини кетериуде халат: %1$s + Контейнерни идентификатору папкагъа джол табалмады: %1$d + Бу джерни сураты джокъду, хайда бирин эт! + Бу джерни алайсыз да сураты барды. + Бу джерни сураты болуб-болмагъанын тинте турама. diff --git a/app/src/main/res/values-ku/strings.xml b/app/src/main/res/values-ku/strings.xml index d9d5b65b9..506e9e4b4 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 ab657b354..8112afea6 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 02abd4ea1..99fb8c1f7 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 8b2ab6b95..2eb2fcf2f 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 d99e269ab..f80784c56 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. @@ -72,8 +72,9 @@ Dréckt fir de Fichier ze gesinn deen Dir eropgelueden hutt Fichier eroplueden: %s %1$s gëtt eropgelueden - Eroplueden vu(n) %1$s ofschléissen + Eropluede vu(n) %1$s ofschléissen %1$s konnt net eropgeluede ginn + Eropluede vu(n) %1$s pauséiert Dréckt fir nach eng Kéier ze probéieren Tippe fir ze kucken Meng rezent eropgeluede Fichieren @@ -86,6 +87,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 +95,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 +352,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 +364,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 +414,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 +457,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 @@ -498,11 +501,20 @@ Dësen Inhalt mellen Ufroe fir dëse Benotzer ze spären Léiert, wéi een nëtzlech Beschrëftunge schreift + Bild änneren + Plaz änneren Plaz aktualiséiert! Plaz ewechhuelen Plaz-Ewechhuele-Warnung Duerch Plaze sinn d\'Biller méi nëtzlech a besser sichbar. Wëllt Dir wierklech d\'Plaz vun dësem Bild ewechhuelen? Plat ewechgeholl! + Dem Auteur Merci soen + Feeler beim Schécke vun engem Merci un den Auteur. + Fichier erfollegräich gespäichert + Wëllt Dir de GPW-Fichier opmaachen? + Wëllt Dir de KML-Fichier opmaachen? + De KML-Fichier konnt net gespäichert ginn. + De GPX-Fichier konnt net gespäichert ginn. KML-Fichier späicheren GPX-Fichier späicheren @@ -510,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-li/strings.xml b/app/src/main/res/values-li/strings.xml index 1720bfbcb..f477ed8f0 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 cb7bebe41..26a9bc7f7 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 9038eec9d..7a6d9e362 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 c496505ae..6d705c4a7 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 @@ Најава Почекајте… Поднова на толкувања и описи - Почекајте… + Почекајте... Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. @@ -119,6 +119,7 @@ Пребарај категории Пребарајте ги предметите прикажани на сликата или снимката (планина, Лесновски манастир итн.) Зачувај + Преливно мени Превчитај Список (Сè уште нема подигања) @@ -479,7 +480,7 @@ Погл. прочитани Погл. непрочитани Се јави грешка при избирањето на сликите - Почекајте… + Почекајте... Избраните слики се дела на високообучени фотографи и илустратори кои заедницата ги избрала за да бидат истакнати како едни од најдобрите слики на Ризницата. Сликите подигнати преку „Околни места“ се оние подигнати при откривање на места на картата. Ова им дава можност на уредниците да им испраќаат благодарници на корисниците што вршат полезни уредувања. Ова се прави стискајќи на малата врска за заблагодарување во страницата за историја или разлики. @@ -501,7 +502,7 @@ Одибиен пристапот до местоположбата на сликата Можеби нема да можеме автоматски да ги добиеме податоците за местоположба од сликите што ги подигате. Ставете ја соодветната местоположба за секоја слика пред да подигате Подигајте слики непосредно на Ризницата од телефон. Преземете го прилогот на Ризницата сега: %1$s - Сподели преку… + Сподели преку... Инфо за сликата Не пронајдов ниедна категорија Не пронајдов ниедно прикажување @@ -652,7 +653,7 @@ Погл. категориска страница Погл. страница на предметот Јазик на прилогот - Острани толкување и опис + Отстрани толкување и опис Прочитајте повеќе На сите јазици Изберете местоположба @@ -780,11 +781,20 @@ Друг проблем или информација (објаснете подолу). Вашите мислења се објавуваат на следнава викистраница: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Дали сигурно сакате да ги откажете сите подигања? - Ги откажувам сите подигања… + Ги откажувам сите подигања... Подигања Во исчекување Неуспешно Не можев да ги вчитам податоците за место + Избриши папка + Потврдете бришење + Дали сигурно сакате да ја избришете папката %1$s, која содржи %2$d ставки? + Избриши + Откажи + Папката %1$s е успешно избришана + Не успеав да ја избришам папката %1$s + Не можев да ја префрлам содржината на папката во ѓубре: %1$s + Не успеав да ја добијам патеката на папката за групата со назнака: %1$d Местово сè уште нема слика. Направете ја! Местово веќе има слика. Проверувам дали местово има слика. diff --git a/app/src/main/res/values-mni/strings.xml b/app/src/main/res/values-mni/strings.xml index 0d8e029a4..de888dcbc 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 27a76b0a7..a6c18bca3 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 965594585..546b43f4f 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 e5dd0f3be..1fce0c0da 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 3b4bf30dc..bc3016666 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -131,7 +131,7 @@ Aanmelden niet mogelijk. Er is een probleem met het netwerk Te veel mislukte pogingen. Probeer het over een paar minuten opnieuw. Deze gebruiker is helaas geblokkeerd op Wikimedia Commons - U moet uw code voor tweefactor-authenticatie opgeven. + U moet uw code voor tweetrapsauthenticatie opgeven. Aanmelden mislukt Uploaden Geef deze verzameling een naam @@ -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) @@ -522,7 +523,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 +600,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 +660,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 +807,16 @@ 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-nqo/strings.xml b/app/src/main/res/values-nqo/strings.xml index 62e01d4d5..434298a44 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 @@ ߐ߲߬ߐ߲߬ߐ߲߫߹ ߞߎ߲߬ߠߊ߬ߝߎ߬ߟߋ߲߬ ߜߘߍ ߟߎ߬ ߦߌߟߡߊ ߟߎ߬ - ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫… + ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫... ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬ ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬ ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬ @@ -161,6 +161,7 @@ ߘߌ߲߬ߞߌߙߊ ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߡߊߢߌߣߌ߲ߠߌ߲ ߏ߬ߞߍ߫ ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ + ߞߐߕߐ߯ ߕߐ߯ ߓߊߟߌߣߍ߲ ߓߘߊ߫ ߦߋ߫ ߊ߬ ߟߊߦߟߍ߬ ߐ߲߬ߐ߲߬ߐ߲߫ ߍ߲߬ߍ߲߫ @@ -175,6 +176,7 @@ ߘߏ߲߬ߖߟߎ߬ߡߊ߬ߟߌ ߡߊ߫ ߡߊߛߐ߫ ߌ ߞߊߘߊ߲߫ ߞߊ߬ ߞߍ߫ ߓߋߕߊ ߣߍߣߍߓߊ߮ ߘߏ߫ ߘߌ߫ + ߌ ߕߐ߮ ߛߓߍ߫ ߊ߲ ߠߊ߫ ߓߋߕߊ ߥߏ߬ߦߏ ߟߊ߫ ߜ߭ߎߜ߭ߏߟ ߔߑߟߋߦ ߞߊ߲߬ ߞߊ߬ ߗߋߘߊ߫ ߞߎߘߊ ߟߎ߬ ߟߊߛߐ߬ߘߐ߲߫ ߊ߬ ߣߌ߫ ߞߐߕߐ߯ ߕߌߢߍߣߍ߲ ߠߎ߬ 2FA Code ߌ ߦߴߊ߬ ߝߍ߬ ߞߵߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ߫ ߝߛߊߦߌ߫؟ ߡߋߘߌߦߊ ߖߌ߬ߦߊ߬ߓߍ ߓߘߊ߫ ߗߌߙߏ߲߫ @@ -196,6 +198,7 @@ ߞߊ߬ ߓߍ߲߬ ߟߊ߬ߓߍ߲߬ߢߐ߲߰ߡߦߊ߬ߘߊ ߞߙߐ߬ߛߌ߬ߕߊ + ߌ ߕߊ߫ ߦߋߕߊ ߗߋ߫ ߜ߭ߌߕߑߤߐߕ ߛߌߟߊ ߝߍ߬ ߌ ߜߊ߲߬ߞߎ߲߬ߣߍ߲ ߓߐ߫ ߟߞߊ߬ߙߊ߲߬ߠߌ߲ ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ ߟߎ߬ @@ -227,13 +230,15 @@ ߞߍߦߙߐ߫ ߞߐߜߍ ߘߐߜߍ߫ ߝߊߙߊ߲ߝߊ߯ߛߌ߫ ߞߏ ߘߐ߫ ߊ߬ ߟߊߜߊ߲߫ ߌ ߜߊ߲߬ߞߎ߲߬ - ߌ ߦߴߊ߬ ߝߍ߬ ߓߊ߬ ߞߵߌ ߜߊ߲߫ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߞߎ߲߬ߠߊ߫؟ - ߌ ߞߊߞߊ߲߫ ߞߵߌ ߜߊ߲߬ߞߎ߲߫ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߟߊߦߍ߬ߟߍ߫ ߛߐ߲߬. + ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߴߦߋ߲߬ ߝߛߊߦߌ߫؟ + ߌ ߞߊ߫ ߞߊ߲߫ ߞߵߌ ߜߊ߲߬ߞߎ߲߫ ߡߎߣߎ߲߬ ߛߴߌ ߘߴߛߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߊߦߍ߬ߟߍ߫ ߢߍߝߍ߬. ߌ ߜߊ߲߬ߞߎ߲߫ ߖߊ߰ߣߌ߲߫ ߞߊ߬ ߟߊ߬ߓߊ߰ߙߊ߬ߟߌ ߣߌ߲߬ ߠߊߓߊ߯ߙߊ߫ ߥߞߌߛߓߍߟߌ ߓߊߓߌ߬ߟߊ߬ ߛߓߍߝߌߘߊ-ߜߍ߬ߙߍ߲߬ߘߍ߬ߣߍ߲ ߘߐ߫ ߥߞߌߛߓߍߟߌ ߓߘߊ߫ ߓߊ߲߫ ߓߊߓߌ߬ߟߊ߬ ߟߊ߫ ߛߓߍߝߌߘߊ-ߜߍ߬ߙߍ߲߬ߘߍ߬ߣߍ߲ ߘߐ߫ ߕߙߐ߬ߝߍ߬ߟߊ ߓߊ߯ߙߊߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߴߊ߬ ߢߊߓߘߍ ߡߊ߬߸ ߘߌ߲߬ߞߌߙߊ ߕߴߦߋ߲߬. + ߓߊ߲߬ ߓߘߊ߫ ߞߍ߫ ߘߌ߲߬ߞߌߙߊ ߡߊߛߐ߬ߘߐ߲ ߡߊ߬. ߌ ߘߌ߲߬ߞߌߙߊ ߘߊ߲߬ߠߊߕߍ߰ ߖߊ߰ߣߌ߲߫ ߛߴߌ ߘߌ߫ ߗߋߘߊ ߣߌ߲߬ ߠߊߛߐ߬ߘߐ߲߫. ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲߫ ߦߋ߫ ߞߊ߬ ߛߌ߬ߢߐ߲߰ߦߊ߫ ߛߙߍߘߍ ߟߎ߬ ߟߊߓߊ߯ߙߊ߫ + ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲߫ ߦߋ߫ ߞߊ߬ ߛߌ߬ߢߐ߲߰ ߖߌߦߊߓߍ ߟߎ߬ ߛߙߍߘߍ ߟߊߓߊ߯ߙߊ߫ ߞߎ߲߬ߕߋߟߋ߲ ߥߞߌߘߕߊ ߥߞߌߔߋߘߌߦߊ߫ @@ -277,6 +282,7 @@ ߟߊ߬ߦߟߍ߬ߣߍ߲ ߠߎ߬ ߜߋߟߋ߲ߜߋߟߋ߲ ߠߊ߫ ߔߊ߬ߔߘߊ ߖߌ߬ߦߊ߬ߓߍ ߓߌ߬ߟߊ߬ߣߍ߲߬ ߦߋ߫ %1$s ߟߊ߫ ߥߞߌߘߕߊ ߛߌߟߊ ߟߋ߬ ߝߍ߬߹ + ߥߞߌߘߕߊ ߞߣߐߘߐ ߟߎ߬ ߓߘߊ߫ ߞߢߊ߬ ߟߊߞߎߘߦߊ߫ ߟߊ߫߹ ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. ߘߊ߬ߣߊ߲߬ߥߟߊ ߓߌ߬ߟߊ ߓߘߊ߫ ߛߎߘߊ߲߫߹ ߡߊ߬ߝߍ߬ߣߍ߲߬ߠߌ߲ @@ -290,14 +296,16 @@ ߖߌ߬ߦߊ߬ߓߍ ߡߍ߲ ߠߎ߬ ߦߋ߫ ߛߋߒߞߏߟߦߊ ߦߌ߬ߘߊ߬ ߟߊ߫ ߥߟߊ߫ ߟߐ߲ߠߌ߲ߦߊ߸ ߏ߬ ߟߎ߬ ߟߊߛߣߍߣߍ߲ߓߊ ߟߋ߬ ߦߋ߫ ߞߐߡߐ߲ߛ ߞߊ߲߬. ߌ ߓߘߊ߫ %1$s ߖߋ߬ߓߟߌ߬ ߓߘߍ ߛߐ߬ߘߐ߲߬. ߌ ߞߎߟߎ߲ߖߋ߫߹ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߝߌ߬ߟߊ ߘߏ߫ ߘߐ߫ ߞߋߟߋ߲߫ ߢߣߊߕߊߟߌ ߓߊߕߐ߬ߡߐ߲߫ ߞߊ߬ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߖߋ߬ߓߌ߫ - ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߥߎ߬ߛߎ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬߸ ߌ ߜߊ߲߬ߞߎ߲߬ ߕߎ߲߯ߣߌ߲߫ ߖߊ߰ߣߌ߲߫. + ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߥߎ߬ߛߎ ߓߘߊ߫ ߕߊ߬ߡߌ߲߬߸ ߌ ߜߊ߲߬ߞߎ߲߬ ߌߞߐ߫ ߖߊ߰ߣߌ߲߫. ߌ ߟߊ߫ ߢߌ߬ߣߌ߲߬ߞߊ߬ߟߌ ߟߊߖߍ߲ߛߍ߲߫ ߌ ߕߋߙߌ ߟߎ߬ ߡߊ߬߹ ߘߊߓߊ߲ߠߌ߲ ߖߋ߬ߓߟߌ߬ ߢߌߡߊ ߖߋ߬ߓߌ߬ߟߌ ߝߘߏ߬ߣߍ߲ ߊ߬ ߝߐ߫ ߘߊ߬ߣߊ߲߬ߥߟߊ߬ߖߌߦߊ ߣߌ߲߬ ߓߍ߲߬ߣߍ߲߫ ߦߋ߫ ߟߊ߬ߦߟߍ߬ߟߌ ߘߐ߫ ߝߋߎ߫؟ ߟߥߊ߬ߟߌ߬ߟߊ߲ ߟߊ߬ߖߍ߲߬ߛߍ߲߬ߠߌ߲ - ߝߌ߬ߟߌ ߦߋ߫ ߛߌ߰ߢߐ߲߰ ߘߌ߲ߞߌߙߊ ߟߊߘߏ߲߬ߕߐ ߘߐ߫. + ߡߊߖߍ߲߬ߞߍ߫ + ߛߌ߰ߢߐ߲߰ ߘߌ߲ߞߌߙߊ ߕߴߛߋ߫ ߟߊߢߎ߲߫ ߠߊ߫. + ߖߌ߬ߦߊ߬ߓߍ߫ ߕߍ߫ ߘߌ߲߬ߞߌߙߊ ߣߌ߲߬ ߞߊ߲߬ ߛߌ߰ߢߐ߲߰ ߦߙߐ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߫ ߟߊ߬ߡߌߣߌ߲ ߘߐ߫ ߝߟߌ߬ ߓߘߊ߫ ߞߍ߫ ߛߌ߰ߢߐ߲߰ ߝߙߎߕߎ ߟߊߛߐ߬ߘߐ߲ ߘߐ߫. ߢߌߣߌ߲ߠߌ߲߫ ߠߊߓߊ߲ ߕߴߦߋ߲߬ @@ -408,7 +416,7 @@ ߘߐ߬ߞߊ߬ߙߊ߲߬ߣߍ߲ ߠߎ߬ ߦߋ߫ ߘߐ߬ߞߊ߬ߙߊ߲߬ߓߊߟߌ ߟߎ߬ ߦߋ߫ ߝߎ߬ߕߎ߲߬ߕߌ ߓߌ߬ߟߊ߬ߣߍ߲߫ ߊ߬ ߘߐ߫ ߞߵߌ ߕߏ߫ ߖߌ߬ߦߊ߬ߓߍ ߓߊߕߐ߬ߡߐ߲ ߞߊ߲߬. - ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬… + ߡߊ߬ߞߐ߬ߣߐ߲߬ߠߌ߲ ߞߍ߫ ߖߊ߰ߣߌ߲߬... ߓߘߊ߫ ߓߊߓߌ߬ߟߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߢߌ߬ߡߊ߬ ߟߊߦߟߍ߬ߕߊ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ ߖߌ߬ߦߊ߬ߓߍ߬ ߖߎ߰ ߟߊߦߟߍ߬ߓߊߟߌ ߟߎ߬ ߞߏߟߊ߲ߞߏߡߊ @@ -418,7 +426,7 @@ ߘߌ߲߬ߞߌߙߊ ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ ߛߎ߲ߝߘߍ - ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬… + ߟߥߊ߬ߟߌ߬ߟߊ߲ ߠߊߖߍ߲ߛߍ߲ ߢߌ߲߬ ߠߎ߫ ߞߊ߲߬... ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬ ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬ @@ -486,7 +494,7 @@ ߊ߬ ߓߌ߬ߟߊ߬ ߟߊ߬ߡߊ ߘߐ߫ ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߫ ߕߎ߲߬ ߕߍ߫. ߘߊ߲߬ߘߊ߲߬ߥߟߊ ߕߍ߫ ߛߐ߲߬ ߘߐߓߍ߲߬ ߠߊ߫. ߊ߬ ߓߌ߬ߟߊ߬ ߘߊ߬ߣߊ߲߬ߥߟߊ ߟߊ߫. - ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫… + ߘߊ߬ߣߊ߲߬ߥߟߊ ߘߊߘߐߓߍ߲߭ ߦߴߌ ߘߐ߫. ߌ ߟߐ߬ ߖߊ߰ߣߌ߲߫... ߞߊ߲ߞߋ ߟߊߓߊ߬ߕߏ߬ ߘߌ߬ߓߌ ߦߋߟߋ߲ @@ -533,9 +541,9 @@ ߟߊߓߊ߯ߙߊߣߍ߲ ߒ ߠߊ߫ ߛߝߊ ߖߌ߬ߦߊ߬ߓߍ ߛߎ߯ߦߊ - ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫… - ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫… + ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߡߌ߬ߣߊ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߟߊߟߐ߭ ߦߴߌ ߘߐ߫... + ߟߊ߬ߦߟߍ߬ߟߌ ߘߐߛߊ߭ ߦߴߌ ߘߐ߫... ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬ ߡߋߘߌߦߊ ߝߊߙߊ߲ߝߊ߯ߛߌ ߦߌߟߡߊ߫ ߞߐߜߍ ߘߐߜߍ߫ diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index eab67e076..c097898e9 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 d0ee73396..2733093d4 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 @@ ਪਾਰਸ਼ਬਦ ਭੁੱਲ ਗਏ? ਦਾਖ਼ਲਾ ਹੋ ਰਿਹਾ ਹੈ ਉਡੀਕੋ ਜੀ… - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਦਾਖ਼ਲ ਹੋਣਾ ਸਫ਼ਲ! ਦਾਖ਼ਲ ਹੋਣਾ ਅਸਫ਼ਲ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। @@ -97,7 +98,7 @@ ਵਿਚਾਰ ਭੇਜੋ (ਈਮੇਲ ਰਾਹੀਂ) ਕੋਈ ਈਮੇਲ ਸਾਧਨ ਇੰਸਟਾਲ ਨਹੀਂ ਕੀਤਾ ਗਿਆ ਹਾਲ \'ਚ ਵਰਤੀਆਂ ਗਈਆਂ ਸ਼੍ਰੇਣੀਆਂ - ਪਹਿਲੀ ਸਿੰਕ ਲਈ ਉਡੀਕ… + ਪਹਿਲੇ ਸਮਕਾਲੀਕਰਨ ਦੀ ਉਡੀਕ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ... ਤੁਸੀਂ ਹਾਲੇ ਤੱਕ ਕੋਈ ਤਸਵੀਰਾਂ ਅੱਪਲੋਡ ਨਹੀਂ ਕੀਤੀਆਂ ਮੁੜ-ਕੋਸ਼ਿਸ਼ ਕਰੋ ਰੱਦ ਕਰੋ @@ -128,7 +129,7 @@ ਹਾਂ! ਹੋਰ ਜਾਣਕਾਰੀ ਸ਼੍ਰੇਣੀਆਂ - ਲੱਦ ਰਿਹਾ ਹੈ… + ਲੱਦ ਰਿਹਾ ਹੈ... ਕੋਈ ਵੀ ਨਹੀਂ ਚੁਣਿਆ ਕੋਈ ਵੇਰਵਾ ਨਹੀਂ ਕੋਈ ਗੱਲਬਾਤ ਨਹੀਂ @@ -167,7 +168,9 @@ ਵਿਕੀਪੀਡੀਆ ਲੇਖ ਤਸਵੀਰ ਬਹੁਤ ਗੂੜ੍ਹੀ ਹੈ। ਤਸਵੀਰ ਧੁੰਦਲੀ ਹੈ। + ਆਪਣੇ ਖਾਤੇ ਵਿੱਚ ਦਾਖ਼ਲ ਹੋਵੋ ਛੱਡੋ + ਦਾਖ਼ਲ ਹੋਵੋ ਵਿਕੀਡੇਟਾ ਵਿਕੀਪੀਡੀਆ ਅਕਸਰ ਪੁੱਛੇ ਜਾਂਦੇ ਸੁਆਲ @@ -184,12 +187,14 @@ ਸ਼੍ਰੇਣੀਆਂ ਨਕਸ਼ਾ ਸਵਾਲ + ਤੁਹਾਡੇ ਦਾਖਲੇ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਮੁੜ ਦਾਖਲ ਹੋਵੋ। ਜਾਰੀ ਰੱਖੋ ਕੋਈ ਤਾਜ਼ਾ ਖੋਜ ਨਹੀਂ ਮਿਟਾਓ ਪ੍ਰਾਪਤੀਆਂ ਅੰਕੜੇ ਧੰਨਵਾਦ ਪ੍ਰਾਪਤ ਹੋਏ + ਆਪਣੀਆਂ ਪ੍ਰਾਪਤੀਆਂ ਨੂੰ ਆਪਣੇ ਦੋਸਤਾਂ ਨਾਲ ਸਾਂਝਾ ਕਰੋ! ਸੂਚਨਾਵਾਂ (ਪੜ੍ਹਿਆਂ) ਸੂਚੀ ਅੱਗੇ @@ -200,11 +205,14 @@ ਇਜਾਜ਼ਤ ਦਿਓ ਖ਼ਾਰਜ ਕਰੋ ਧੰਨਵਾਦ ਭੇਜਣਾ: ਸਫਲ ਹੋਇਆ - ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ… + ਕਿਰਪਾ ਕਰਕੇ ਉਡੀਕੋ... ਉਤਾਰਾ ਕੀਤਾ ਟਿਕਾਣਾ + ਲਿਖਤ ਚੂੰਢੀ-ਤਖਤੀ \'ਤੇ ਲਾਹੀ ਗਈ ਏ। ਲਿਖਤ ਛਾਪੋ + ਵਿਕੀਕੋਡ ਦਾ ਉਤਾਰਾ ਚੂੰਢੀ-ਤਖਤੀ \'ਤੇ ਲਿਖੋ ਮੁਹਰੈਲ ਵਰਤੋਂਕਾਰ + ਟਿਕਾਣਾ ਨਵਿਆਈਆ ਗਿਆ ਤੁਹਾਡੇ ਦਾਖਲੇ ਦੀ ਮਿਆਦ ਪੁੱਗ ਗਈ ਹੈ। ਕਿਰਪਾ ਕਰਕੇ ਦੁਬਾਰਾ ਦਾਖਲ ਹੋਵੋ। diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 09132f40e..9ea9ecb06 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. @@ -769,6 +769,8 @@ Aby skonfigurować swojego awatara rankingu, dotknij \'Ustaw jako awatar\' w menu z trzema kropkami dowolnego obrazu. Współrzędne nie są dokładnymi współrzędnymi, ale osoba, która przesłała to zdjęcie, uważa, że są wystarczająco blisko. Nie można udostępnić tego elementu + Przeczytaj jak napisać użyteczny opis + Przeczytaj jak napisać użyteczny podpis Edytuj obraz Edytuj lokalizację Lokalizacja zaktualizowana! diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index b7449e957..2ccd3c07a 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) @@ -477,7 +478,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 +500,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 +777,21 @@ À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 + 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 + 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-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 461cb6b1d..4f17da26f 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 b0dd3b016..3779a8a51 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 19a52d72f..bad9dc500 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 ad1d0b805..0bcbc1550 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 861d7ee27..9b95e69d1 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,9 +841,20 @@ Другая проблема или информация (пожалуйста, объясните ниже). Ваш отзыв будет опубликован на следующей вики-странице: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:Mobile app/Feedback</a> Вы уверены, что хотите отменить все загрузки? - Отмена всех загрузок… + Отмена всех загрузок... Загрузки В ожидании Не удалось Не удалось загрузить данные о месте + Удалить папку + Подтвердите удаление + Вы уверены, что хотите удалить папку %1$s содержащую %2$d шт. вложенных элементов? + Удалить + Отмена + Папка %1$s успешно удалена + Не получилось удалить папку %1$s + Ошибка при удалении содержимого папки: %1$s + У этого места пока нет фотографий, так что сделайте несколько! + У этого места уже есть фотография. + Сейчас проверим, есть ли у этого места фотография. diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index d4b591659..08f9a1fec 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 0489c363a..78114e336 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 8e9cde75c..5997677ef 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 0e661acb7..92fa25f3e 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 99a0bf548..49fc88a3b 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-skr/strings.xml b/app/src/main/res/values-skr/strings.xml index 4c68ee91b..f36e6f983 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-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index b91c3c0b1..61531980f 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 f1e7412d4..40e99618f 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 64379ac92..79ae5ea28 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 c49d64ad2..5ffde0b59 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) @@ -481,6 +482,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 @@ -506,7 +508,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,9 +785,21 @@ 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 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. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 4f41da5f6..f9162bc7b 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 13ee985b9..add46f7b7 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 0c478b221..ae80a5335 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 70bee59ef..125bba590 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 79482fb4e..54593c681 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 @@ -502,10 +502,11 @@ Okunmamış bildiriminiz yok Okundu bildiriminiz yok Günlükleri kullanarak paylaş + E-posta kutunuzu denetleyin 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 +605,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 @@ -715,7 +716,7 @@ Yakındaki haritalar düzgün çalışmak için TELEFON DURUMUNU okumaya ihtiyaç duyuyor Kullanıcının katkıları: %s Kullanıcının başarıları: %s - Kullanıcı sayfasını görüntüle + Kullanıcı profilini görüntüle Betimlemeleri düzenle Kategorileri düzenle Gelişmiş Seçenekler @@ -795,5 +796,16 @@ Lütfen çoklu yüklemedeki tüm görsellerin aynı kategorilere ve tasvirlere sahip olduğunu unutmayın. Görseller tasvirleri ve kategorileri paylaşmıyorsa, lütfen birkaç ayrı yükleme gerçekleştirin. Çoklu yüklemelerle ilgili not + Bu öge ile ilgili bir sorunu Vikiveri\'ye bildirin + Lütfen bir yorum girin + Tartışma + \' %1$s \' öğesi hakkında bir şeyler yazın. Herkes tarafından görülebilir olacaktır. + 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? + Tüm yüklemeler iptal ediliyor... + Yüklemeler + Beklemede + Başarısız + Bu yerin zaten bir resmi var. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cc2343a77..9e821ae24 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 f09f76ac6..a708c873a 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 52bb495ea..bbcb64561 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-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 224492ab7..635d71a3f 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,6 +1,7 @@