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
|
- 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.
|
- 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
|
- Provide better messages for OpenKeychain errors
|
||||||
- Rename passwords
|
- Rename passwords and categories
|
||||||
|
|
||||||
### Changed
|
### 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)
|
- **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
|
// Called each time the action mode is shown. Always called after onCreateActionMode, but
|
||||||
// may be called multiple times if the mode is invalidated.
|
// may be called multiple times if the mode is invalidated.
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,6 +173,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
||||||
requireStore().movePasswords(recyclerAdapter.getSelectedItems(requireContext()))
|
requireStore().movePasswords(recyclerAdapter.getSelectedItems(requireContext()))
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
R.id.menu_edit_password -> {
|
||||||
|
requireStore().renameCategory(recyclerAdapter.getSelectedItems(requireContext()))
|
||||||
|
mode.finish()
|
||||||
|
false
|
||||||
|
}
|
||||||
else -> false
|
else -> false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ import com.github.ajalt.timberkt.i
|
||||||
import com.github.ajalt.timberkt.w
|
import com.github.ajalt.timberkt.w
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
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.AutofillMatcher
|
||||||
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
|
import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
|
||||||
import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
|
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.PasswordRepository.PasswordSortOrder.Companion.getSortOrder
|
||||||
import com.zeapo.pwdstore.utils.commitChange
|
import com.zeapo.pwdstore.utils.commitChange
|
||||||
import com.zeapo.pwdstore.utils.listFilesRecursively
|
import com.zeapo.pwdstore.utils.listFilesRecursively
|
||||||
|
import com.zeapo.pwdstore.utils.requestInputFocusOnView
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -628,7 +630,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
)
|
)
|
||||||
.setPositiveButton(R.string.dialog_ok) { _, _ ->
|
.setPositiveButton(R.string.dialog_ok) { _, _ ->
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
movePassword(source, destinationFile)
|
moveFile(source, destinationFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.setNegativeButton(R.string.dialog_cancel, null)
|
.setNegativeButton(R.string.dialog_cancel, null)
|
||||||
|
@ -636,7 +638,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
movePassword(source, destinationFile)
|
moveFile(source, destinationFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -664,6 +666,69 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
}.launch(intent)
|
}.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.
|
* 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)
|
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) {
|
val sourceDestinationMap = if (source.isDirectory) {
|
||||||
|
destinationFile.mkdirs()
|
||||||
// Recursively list all files (not directories) below `source`, then
|
// Recursively list all files (not directories) below `source`, then
|
||||||
// obtain the corresponding target file by resolving the relative path
|
// obtain the corresponding target file by resolving the relative path
|
||||||
// starting at the destination folder.
|
// starting at the destination folder.
|
||||||
|
@ -746,7 +812,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
|
||||||
mapOf(source to destinationFile)
|
mapOf(source to destinationFile)
|
||||||
}
|
}
|
||||||
if (!source.renameTo(destinationFile)) {
|
if (!source.renameTo(destinationFile)) {
|
||||||
e { "Something went wrong while moving." }
|
e { "Something went wrong while moving $source to $destinationFile." }
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
MaterialAlertDialogBuilder(this@PasswordStore)
|
MaterialAlertDialogBuilder(this@PasswordStore)
|
||||||
.setTitle(R.string.password_move_error_title)
|
.setTitle(R.string.password_move_error_title)
|
||||||
|
|
|
@ -20,7 +20,7 @@ class FolderCreationDialogFragment : DialogFragment() {
|
||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
|
val alertDialogBuilder = MaterialAlertDialogBuilder(requireContext())
|
||||||
alertDialogBuilder.setTitle(R.string.title_create_folder)
|
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)) { _, _ ->
|
alertDialogBuilder.setPositiveButton(getString(R.string.button_create)) { _, _ ->
|
||||||
createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!)
|
createDirectory(requireArguments().getString(CURRENT_DIR_EXTRA)!!)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,4 +20,9 @@
|
||||||
android:title="@string/delete"
|
android:title="@string/delete"
|
||||||
app:showAsAction="ifRoom" />
|
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>
|
</menu>
|
||||||
|
|
|
@ -222,6 +222,7 @@
|
||||||
<string name="dialog_positive">Go to Settings</string>
|
<string name="dialog_positive">Go to Settings</string>
|
||||||
<string name="dialog_negative">Go back</string>
|
<string name="dialog_negative">Go back</string>
|
||||||
<string name="dialog_cancel">Cancel</string>
|
<string name="dialog_cancel">Cancel</string>
|
||||||
|
<string name="dialog_skip">Skip</string>
|
||||||
<string name="git_sync">Synchronize repository</string>
|
<string name="git_sync">Synchronize repository</string>
|
||||||
<string name="git_pull">Pull from remote</string>
|
<string name="git_pull">Pull from remote</string>
|
||||||
<string name="git_push">Push to 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_title">Show hidden folders</string>
|
||||||
<string name="pref_show_hidden_summary">Include hidden directories in the password list</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_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="button_create">Create</string>
|
||||||
<string name="pref_search_on_start">Open search on start</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>
|
<string name="pref_search_on_start_hint">Open search bar when app is launched</string>
|
||||||
|
|
Loading…
Reference in a new issue