Add support for category renaming (#854)
* rename category * changed CHANGELOG * IDE Refactor * Address review comments Signed-off-by: Harsh Shandilya <me@msfjarvis.dev> * 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 <me@msfjarvis.dev> Co-authored-by: Harsh Shandilya <msfjarvis@gmail.com> Co-authored-by: Fabian Henneke <fabian@henneke.me> Co-authored-by: Fabian Henneke <FabianHenneke@users.noreply.github.com>
This commit is contained in:
parent
33b3f54921
commit
23b488a8eb
7 changed files with 91 additions and 6 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TextInputEditText>(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<TextInputEditText>(R.id.folder_name_text)
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun renameCategory(categories: List<PasswordItem>) {
|
||||
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)
|
||||
|
|
|
@ -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)!!)
|
||||
}
|
||||
|
|
|
@ -20,4 +20,9 @@
|
|||
android:title="@string/delete"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_edit_password"
|
||||
android:icon="@drawable/ic_edit_white_24dp"
|
||||
android:title="@string/edit"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
|
|
@ -222,6 +222,7 @@
|
|||
<string name="dialog_positive">Go to Settings</string>
|
||||
<string name="dialog_negative">Go back</string>
|
||||
<string name="dialog_cancel">Cancel</string>
|
||||
<string name="dialog_skip">Skip</string>
|
||||
<string name="git_sync">Synchronize repository</string>
|
||||
<string name="git_pull">Pull from remote</string>
|
||||
<string name="git_push">Push to remote</string>
|
||||
|
@ -323,6 +324,11 @@
|
|||
<string name="pref_show_hidden_title">Show hidden folders</string>
|
||||
<string name="pref_show_hidden_summary">Include hidden directories in the password list</string>
|
||||
<string name="title_create_folder">Create folder</string>
|
||||
<string name="title_rename_folder">Rename folder</string>
|
||||
<string name="message_category_error_empty_field">Category name can\'t be empty</string>
|
||||
<string name="message_category_error_category_exists">Category name already exists</string>
|
||||
<string name="message_category_error_destination_outside_repo">Destination must be within the repository</string>
|
||||
<string name="message_rename_folder">Enter destination for %1$s</string>
|
||||
<string name="button_create">Create</string>
|
||||
<string name="pref_search_on_start">Open search on start</string>
|
||||
<string name="pref_search_on_start_hint">Open search bar when app is launched</string>
|
||||
|
|
Loading…
Reference in a new issue