diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f84343f..1dc30484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file. - Add support for better, more secure Keyex's and MACs with a brand new SSH backend - Allow manually marking domains for subdomain-level association. This will allow you to keep separate passwords for `site1.example.com` and `site2.example.com` and have them show as such in Autofill. - Provide better messages for OpenKeychain errors -- Rename passwords +- Rename passwords and categories ### Changed - **BREAKING**: Remove support for HOTP/TOTP secrets - Please use FIDO keys or a dedicated app like [Aegis](https://github.com/beemdevelopment/Aegis) or [andOTP](https://github.com/andOTP/andOTP) 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_dialog_fragment.xml similarity index 100% rename from app/src/main/res/layout/folder_creation_dialog_fragment.xml rename to app/src/main/res/layout/folder_dialog_fragment.xml 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