From 23b488a8eb69c99d5337538cc17690d2b637b9be Mon Sep 17 00:00:00 2001 From: Diogenes Molinares Date: Thu, 18 Jun 2020 14:07:26 +0200 Subject: Add support for category renaming (#854) * rename category * changed CHANGELOG * IDE Refactor * Address review comments Signed-off-by: Harsh Shandilya * change Stack to List and fix bug when empty category name * create intermediate folders * little fixes and KDoc added * Reuse existing move code * change button Cancel => Skip * use canonicalPath to confirm destination inside repository * change error message * update KDoc * show different error to user Co-authored-by: Harsh Shandilya Co-authored-by: Harsh Shandilya Co-authored-by: Fabian Henneke Co-authored-by: Fabian Henneke --- .../java/com/zeapo/pwdstore/PasswordFragment.kt | 8 +++ .../main/java/com/zeapo/pwdstore/PasswordStore.kt | 74 ++++++++++++++++++++-- .../ui/dialogs/FolderCreationDialogFragment.kt | 2 +- .../res/layout/folder_creation_dialog_fragment.xml | 25 -------- app/src/main/res/layout/folder_dialog_fragment.xml | 25 ++++++++ app/src/main/res/menu/context_pass.xml | 5 ++ app/src/main/res/values/strings.xml | 6 ++ 7 files changed, 115 insertions(+), 30 deletions(-) delete mode 100644 app/src/main/res/layout/folder_creation_dialog_fragment.xml create mode 100644 app/src/main/res/layout/folder_dialog_fragment.xml (limited to 'app/src/main') diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt index f4c1685e..a47e6e70 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt @@ -154,6 +154,9 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { // Called each time the action mode is shown. Always called after onCreateActionMode, but // may be called multiple times if the mode is invalidated. override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + menu.findItem(R.id.menu_edit_password).isVisible = + recyclerAdapter.getSelectedItems(requireContext()) + .all { it.type == PasswordItem.TYPE_CATEGORY } return true } @@ -170,6 +173,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) { requireStore().movePasswords(recyclerAdapter.getSelectedItems(requireContext())) false } + R.id.menu_edit_password -> { + requireStore().renameCategory(recyclerAdapter.getSelectedItems(requireContext())) + mode.finish() + false + } else -> false } } diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt index 0e03804f..b0f6fd44 100644 --- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt +++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt @@ -43,6 +43,7 @@ import com.github.ajalt.timberkt.i import com.github.ajalt.timberkt.w import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputEditText import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel @@ -66,6 +67,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder import com.zeapo.pwdstore.utils.commitChange import com.zeapo.pwdstore.utils.listFilesRecursively +import com.zeapo.pwdstore.utils.requestInputFocusOnView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -628,7 +630,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { ) .setPositiveButton(R.string.dialog_ok) { _, _ -> launch(Dispatchers.IO) { - movePassword(source, destinationFile) + moveFile(source, destinationFile) } } .setNegativeButton(R.string.dialog_cancel, null) @@ -636,7 +638,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { } } else { launch(Dispatchers.IO) { - movePassword(source, destinationFile) + moveFile(source, destinationFile) } } } @@ -664,6 +666,69 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { }.launch(intent) } + private fun isInsideRepository(file: File): Boolean { + return file.canonicalPath.contains(getRepositoryDirectory(this).canonicalPath) + } + + enum class CategoryRenameError(val resource: Int) { + None(0), + EmptyField(R.string.message_category_error_empty_field), + CategoryExists(R.string.message_category_error_category_exists), + DestinationOutsideRepo(R.string.message_category_error_destination_outside_repo), + } + + /** + * Prompt the user with a new category name to assign, + * if the new category forms/leads a path (i.e. contains "/"), intermediate directories will be created + * and new category will be placed inside. + * + * @param oldCategory The category to change its name + * @param error Determines whether to show an error to the user in the alert dialog, + * this error may be due to the new category the user entered already exists or the field was empty or the + * destination path is outside the repository + * + * @see [CategoryRenameError] + * @see [isInsideRepository] + */ + private fun renameCategory(oldCategory: PasswordItem, error: CategoryRenameError = CategoryRenameError.None) { + val view = layoutInflater.inflate(R.layout.folder_dialog_fragment, null) + val newCategoryEditText = view.findViewById(R.id.folder_name_text) + + if (error != CategoryRenameError.None) { + newCategoryEditText.error = getString(error.resource) + } + + val dialog = MaterialAlertDialogBuilder(this) + .setTitle(R.string.title_rename_folder) + .setView(view) + .setMessage(getString(R.string.message_rename_folder, oldCategory.name)) + .setPositiveButton(R.string.dialog_ok) { _, _ -> + val newCategory = File("${oldCategory.file.parent}/${newCategoryEditText.text}") + when { + newCategoryEditText.text.isNullOrBlank() -> renameCategory(oldCategory, CategoryRenameError.EmptyField) + newCategory.exists() -> renameCategory(oldCategory, CategoryRenameError.CategoryExists) + !isInsideRepository(newCategory) -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo) + else -> lifecycleScope.launch(Dispatchers.IO) { + moveFile(oldCategory.file, newCategory) + withContext(Dispatchers.Main) { + commitChange(resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name)) + } + } + } + } + .setNegativeButton(R.string.dialog_skip, null) + .create() + + dialog.requestInputFocusOnView(R.id.folder_name_text) + dialog.show() + } + + fun renameCategory(categories: List) { + for (oldCategory in categories) { + renameCategory(oldCategory) + } + } + /** * Resets navigation to the repository root and refreshes the password list accordingly. * @@ -736,8 +801,9 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { super.onActivityResult(requestCode, resultCode, data) } - private suspend fun movePassword(source: File, destinationFile: File) { + private suspend fun moveFile(source: File, destinationFile: File) { val sourceDestinationMap = if (source.isDirectory) { + destinationFile.mkdirs() // Recursively list all files (not directories) below `source`, then // obtain the corresponding target file by resolving the relative path // starting at the destination folder. @@ -746,7 +812,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) { mapOf(source to destinationFile) } if (!source.renameTo(destinationFile)) { - e { "Something went wrong while moving." } + e { "Something went wrong while moving $source to $destinationFile." } withContext(Dispatchers.Main) { MaterialAlertDialogBuilder(this@PasswordStore) .setTitle(R.string.password_move_error_title) diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt index a5ff0bc1..2f268c56 100644 --- a/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt +++ b/app/src/main/java/com/zeapo/pwdstore/ui/dialogs/FolderCreationDialogFragment.kt @@ -20,7 +20,7 @@ class FolderCreationDialogFragment : DialogFragment() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext()) alertDialogBuilder.setTitle(R.string.title_create_folder) - alertDialogBuilder.setView(R.layout.folder_creation_dialog_fragment) + alertDialogBuilder.setView(R.layout.folder_dialog_fragment) alertDialogBuilder.setPositiveButton(getString(R.string.button_create)) { _, _ -> createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!) } diff --git a/app/src/main/res/layout/folder_creation_dialog_fragment.xml b/app/src/main/res/layout/folder_creation_dialog_fragment.xml deleted file mode 100644 index bc078e64..00000000 --- a/app/src/main/res/layout/folder_creation_dialog_fragment.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/folder_dialog_fragment.xml b/app/src/main/res/layout/folder_dialog_fragment.xml new file mode 100644 index 00000000..bc078e64 --- /dev/null +++ b/app/src/main/res/layout/folder_dialog_fragment.xml @@ -0,0 +1,25 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/context_pass.xml b/app/src/main/res/menu/context_pass.xml index 41a1f705..9c76fca8 100644 --- a/app/src/main/res/menu/context_pass.xml +++ b/app/src/main/res/menu/context_pass.xml @@ -20,4 +20,9 @@ android:title="@string/delete" app:showAsAction="ifRoom" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b65d84f2..58126392 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -222,6 +222,7 @@ Go to Settings Go back Cancel + Skip Synchronize repository Pull from remote Push to remote @@ -323,6 +324,11 @@ Show hidden folders Include hidden directories in the password list Create folder + Rename folder + Category name can\'t be empty + Category name already exists + Destination must be within the repository + Enter destination for %1$s Create Open search on start Open search bar when app is launched -- cgit v1.2.3