mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
custom-selector: adds a button to delete the current folder in custom selector (#5925)
* Issue #5811: "Delete folder" menu in custom image selector * Issue 5811: folder deletion for api < 29. * Issue 5811: folder deletion for api < 29. * Issue 5811: folder deletion for api 29. * Issue 5811: folder deletion * Issue 5811: fixes merge conflicts, replaces used function onActivityResult with an ActivityResultLauncher * Update Constants.java --------- Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
parent
17a8845dfd
commit
634bc3ede1
11 changed files with 447 additions and 12 deletions
|
|
@ -93,7 +93,6 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
implementation "org.jetbrains.kotlin:kotlin-reflect:$KOTLIN_VERSION"
|
||||||
|
|
||||||
//Mocking
|
//Mocking
|
||||||
testImplementation("io.mockk:mockk:1.13.4")
|
|
||||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||||
testImplementation 'org.mockito:mockito-core:5.6.0'
|
testImplementation 'org.mockito:mockito-core:5.6.0'
|
||||||
|
|
@ -367,11 +366,11 @@ android {
|
||||||
|
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_11
|
sourceCompatibility JavaVersion.VERSION_17
|
||||||
targetCompatibility JavaVersion.VERSION_11
|
targetCompatibility JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "11"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildToolsVersion buildToolsVersion
|
buildToolsVersion buildToolsVersion
|
||||||
|
|
|
||||||
|
|
@ -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<IntentSenderRequest>,
|
||||||
|
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<IntentSenderRequest>): 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<IntentSenderRequest>): Boolean
|
||||||
|
{
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return false
|
||||||
|
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
val folderPath = folder.absolutePath
|
||||||
|
val urisToTrash = mutableListOf<Uri>()
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -12,8 +12,10 @@ import android.view.View
|
||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.widget.Button
|
import android.widget.Button
|
||||||
import android.widget.ImageButton
|
import android.widget.ImageButton
|
||||||
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
|
@ -43,13 +45,13 @@ import fr.free.nrw.commons.R
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatus
|
||||||
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
import fr.free.nrw.commons.customselector.database.NotForUploadStatusDao
|
||||||
import fr.free.nrw.commons.customselector.helper.CustomSelectorConstants
|
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.FolderClickListener
|
||||||
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
import fr.free.nrw.commons.customselector.listeners.ImageSelectListener
|
||||||
import fr.free.nrw.commons.customselector.model.Image
|
import fr.free.nrw.commons.customselector.model.Image
|
||||||
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
|
||||||
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
|
||||||
import fr.free.nrw.commons.filepicker.Constants
|
|
||||||
import fr.free.nrw.commons.media.ZoomableActivity
|
import fr.free.nrw.commons.media.ZoomableActivity
|
||||||
import fr.free.nrw.commons.theme.BaseActivity
|
import fr.free.nrw.commons.theme.BaseActivity
|
||||||
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
|
|
@ -148,10 +150,23 @@ class CustomSelectorActivity :
|
||||||
|
|
||||||
private var showPartialAccessIndicator by mutableStateOf(false)
|
private var showPartialAccessIndicator by mutableStateOf(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 ->
|
private val startForResult = registerForActivityResult(StartActivityForResult()){ result ->
|
||||||
onFullScreenDataReceived(result)
|
onFullScreenDataReceived(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* onCreate Activity, sets theme, initialises the view model, setup view.
|
* onCreate Activity, sets theme, initialises the view model, setup view.
|
||||||
*/
|
*/
|
||||||
|
|
@ -239,6 +254,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.
|
* Show Custom Selector Welcome Dialog.
|
||||||
*/
|
*/
|
||||||
|
|
@ -420,10 +444,97 @@ class CustomSelectorActivity :
|
||||||
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
val limitError: ImageButton = findViewById(R.id.image_limit_error)
|
||||||
limitError.visibility = View.INVISIBLE
|
limitError.visibility = View.INVISIBLE
|
||||||
limitError.setOnClickListener { displayUploadLimitWarning() }
|
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(
|
override fun onFolderClick(
|
||||||
folderId: Long,
|
folderId: Long,
|
||||||
|
|
@ -441,6 +552,11 @@ class CustomSelectorActivity :
|
||||||
bucketId = folderId
|
bucketId = folderId
|
||||||
bucketName = folderName
|
bucketName = folderName
|
||||||
isImageFragmentOpen = true
|
isImageFragmentOpen = true
|
||||||
|
|
||||||
|
//show the overflow menu only when a folder is clicked
|
||||||
|
showOverflowMenu = true
|
||||||
|
setUpToolbar()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -556,6 +672,10 @@ class CustomSelectorActivity :
|
||||||
isImageFragmentOpen = false
|
isImageFragmentOpen = false
|
||||||
changeTitle(getString(R.string.custom_selector_title), 0)
|
changeTitle(getString(R.string.custom_selector_title), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//hide overflow menu when not in folder
|
||||||
|
showOverflowMenu = false
|
||||||
|
setUpToolbar()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import android.text.Editable;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
import android.widget.AdapterView.OnItemClickListener;
|
||||||
|
|
@ -59,6 +60,7 @@ import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
public class SettingsFragment extends PreferenceFragmentCompat {
|
public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
|
|
@ -81,6 +83,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
private ListPreference themeListPreference;
|
private ListPreference themeListPreference;
|
||||||
private Preference descriptionLanguageListPreference;
|
private Preference descriptionLanguageListPreference;
|
||||||
private Preference appUiLanguageListPreference;
|
private Preference appUiLanguageListPreference;
|
||||||
|
private Preference showDeletionButtonPreference;
|
||||||
private String keyLanguageListPreference;
|
private String keyLanguageListPreference;
|
||||||
private TextView recentLanguagesTextView;
|
private TextView recentLanguagesTextView;
|
||||||
private View separator;
|
private View separator;
|
||||||
|
|
@ -188,6 +191,18 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//
|
||||||
|
showDeletionButtonPreference = findPreference("displayDeletionButton");
|
||||||
|
if (showDeletionButtonPreference != null) {
|
||||||
|
showDeletionButtonPreference.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
boolean isEnabled = (boolean) newValue;
|
||||||
|
// Save preference when user toggles the button
|
||||||
|
defaultKvStore.putBoolean("displayDeletionButton", isEnabled);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Preference betaTesterPreference = findPreference("becomeBetaTester");
|
Preference betaTesterPreference = findPreference("becomeBetaTester");
|
||||||
betaTesterPreference.setOnPreferenceClickListener(preference -> {
|
betaTesterPreference.setOnPreferenceClickListener(preference -> {
|
||||||
Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link)));
|
Utils.handleWebUrl(getActivity(), Uri.parse(getResources().getString(R.string.beta_opt_in_link)));
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,15 @@ public class PermissionUtils {
|
||||||
return new String[]{ Manifest.permission.READ_MEDIA_IMAGES,
|
return new String[]{ Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
Manifest. permission.ACCESS_MEDIA_LOCATION };
|
Manifest. permission.ACCESS_MEDIA_LOCATION };
|
||||||
}
|
}
|
||||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
|
||||||
return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE,
|
return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
Manifest.permission.ACCESS_MEDIA_LOCATION };
|
Manifest.permission.ACCESS_MEDIA_LOCATION };
|
||||||
}
|
}
|
||||||
|
if(Build.VERSION.SDK_INT == Build.VERSION_CODES.Q) {
|
||||||
|
return new String[]{ Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
Manifest.permission.ACCESS_MEDIA_LOCATION };
|
||||||
|
}
|
||||||
return new String[]{
|
return new String[]{
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE };
|
Manifest.permission.WRITE_EXTERNAL_STORAGE };
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
android:id="@+id/partial_access_indicator"
|
android:id="@+id/partial_access_indicator"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar_layout"/>
|
app:layout_constraintTop_toBottomOf="@id/toolbar_layout" />
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<androidx.fragment.app.FragmentContainerView
|
||||||
android:id="@+id/fragment_container"
|
android:id="@+id/fragment_container"
|
||||||
|
|
|
||||||
|
|
@ -40,17 +40,36 @@
|
||||||
app:layout_constraintStart_toEndOf="@+id/back"
|
app:layout_constraintStart_toEndOf="@+id/back"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<!-- Warning Icon (image_limit_error) -->
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/image_limit_error"
|
android:id="@+id/image_limit_error"
|
||||||
android:layout_width="48dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
android:background="#00FFFFFF"
|
android:background="#00FFFFFF"
|
||||||
|
android:padding="@dimen/standard_gap"
|
||||||
android:contentDescription="@string/custom_selector_limit_error_desc"
|
android:contentDescription="@string/custom_selector_limit_error_desc"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/menu_overflow"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1.0"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/title"
|
||||||
|
app:srcCompat="@drawable/ic_error_red_24dp" />
|
||||||
|
|
||||||
|
<!-- Overflow Menu Icon (menu_overflow) -->
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/menu_overflow"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="#00FFFFFF"
|
||||||
|
android:padding="@dimen/standard_gap"
|
||||||
|
android:contentDescription="@string/menu_overflow_desc"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_bias="1.0"
|
app:layout_constraintVertical_bias="1.0"
|
||||||
app:srcCompat="@drawable/ic_error_red_24dp" />
|
app:layout_constraintStart_toEndOf="@id/image_limit_error"
|
||||||
|
app:srcCompat="@drawable/ic_overflow" />
|
||||||
|
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</merge>
|
</merge>
|
||||||
9
app/src/main/res/menu/menu_custom_selector.xml
Normal file
9
app/src/main/res/menu/menu_custom_selector.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_delete_folder"
|
||||||
|
android:title="@string/custom_selector_delete_folder"
|
||||||
|
android:icon="@drawable/ic_delete_grey_700_24dp"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
</menu>
|
||||||
|
|
@ -117,6 +117,7 @@
|
||||||
<string name="categories_search_text_hint">Search categories</string>
|
<string name="categories_search_text_hint">Search categories</string>
|
||||||
<string name="depicts_search_text_hint">Search for items that your media depicts (mountain, Taj Mahal, etc.)</string>
|
<string name="depicts_search_text_hint">Search for items that your media depicts (mountain, Taj Mahal, etc.)</string>
|
||||||
<string name="menu_save_categories">Save</string>
|
<string name="menu_save_categories">Save</string>
|
||||||
|
<string name="menu_overflow_desc">Overflow menu</string>
|
||||||
<string name="refresh_button">Refresh</string>
|
<string name="refresh_button">Refresh</string>
|
||||||
<string name="display_list_button">List</string>
|
<string name="display_list_button">List</string>
|
||||||
<string name="contributions_subtitle_zero">(No uploads yet)</string>
|
<string name="contributions_subtitle_zero">(No uploads yet)</string>
|
||||||
|
|
@ -832,6 +833,17 @@ Upload your first media by tapping on the add button.</string>
|
||||||
<string name="pending">Pending</string>
|
<string name="pending">Pending</string>
|
||||||
<string name="failed">Failed</string>
|
<string name="failed">Failed</string>
|
||||||
<string name="could_not_load_place_data">Could not load place data</string>
|
<string name="could_not_load_place_data">Could not load place data</string>
|
||||||
|
|
||||||
|
<string name="custom_selector_delete_folder">Delete Folder</string>
|
||||||
|
<string name="custom_selector_confirm_deletion_title">Confirm Deletion</string>
|
||||||
|
<string name="custom_selector_confirm_deletion_message">Are you sure you want to delete folder %1$s containing %2$d items?</string>
|
||||||
|
<string name="custom_selector_delete">Delete</string>
|
||||||
|
<string name="custom_selector_cancel">Cancel</string>
|
||||||
|
<string name="custom_selector_folder_deleted_success">Folder %1$s deleted successfully</string>
|
||||||
|
<string name="custom_selector_folder_deleted_failure">Failed to delete folder %1$s</string>
|
||||||
|
<string name="custom_selector_error_trashing_folder_contents">Error in trashing folder contents: %1$s</string>
|
||||||
|
<string name="custom_selector_folder_not_found_error">Failed to retrieve folder path for bucket ID: %1$d</string>
|
||||||
|
|
||||||
<string name="red_pin">This place has no picture yet, go take one!</string>
|
<string name="red_pin">This place has no picture yet, go take one!</string>
|
||||||
<string name="green_pin">This place has a picture already.</string>
|
<string name="green_pin">This place has a picture already.</string>
|
||||||
<string name="grey_pin">Now checking whether this place has a picture.</string>
|
<string name="grey_pin">Now checking whether this place has a picture.</string>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,13 @@
|
||||||
android:summary="@string/display_campaigns_explanation"
|
android:summary="@string/display_campaigns_explanation"
|
||||||
android:title="@string/display_campaigns" />
|
android:title="@string/display_campaigns" />
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="displayDeletionButton"
|
||||||
|
app:singleLineTitle="false"
|
||||||
|
android:summary="Enable the "Delete folder" button in the custom picker"
|
||||||
|
android:title="Show Deletion Button" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue