Scroll to files and enter folders when created (#909)

This commit is contained in:
Fabian Henneke 2020-07-01 18:18:21 +02:00 committed by GitHub
parent c5a93b8b81
commit 1c9f7971ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 56 additions and 12 deletions

View file

@ -14,6 +14,7 @@ All notable changes to this project will be documented in this file.
- TOTP support is reintroduced by popular demand. HOTP continues to be unsupported and heavily discouraged.
- Initial support for detecting and filling OTP fields with Autofill
- Importing TOTP secrets using QR codes
- Navigate into newly created folders and scroll to newly created passwords
## [1.9.2] - 2020-06-30

View file

@ -43,6 +43,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
private var recyclerViewStateToRestore: Parcelable? = null
private var actionMode: ActionMode? = null
private var scrollTarget: File? = null
private val model: SearchableRepositoryViewModel by activityViewModels()
private val binding by viewBinding(PasswordRecyclerViewBinding::bind)
@ -132,6 +133,11 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
// When the result is filtered, we always scroll to the top since that is where
// the best fuzzy match appears.
recyclerView.scrollToPosition(0)
} else if (scrollTarget != null) {
scrollTarget?.let {
recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
}
scrollTarget == null
} else {
// When the result is not filtered and there is a saved scroll position for it,
// we try to restore it.
@ -223,12 +229,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
listener = object : OnFragmentInteractionListener {
override fun onFragmentInteraction(item: PasswordItem) {
if (item.type == PasswordItem.TYPE_CATEGORY) {
requireStore().clearSearch()
model.navigateTo(
item.file,
recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
)
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
navigateTo(item.file)
} else {
if (requireArguments().getBoolean("matchWith", false)) {
requireStore().matchPasswordWithApp(item)
@ -272,6 +273,19 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
fun createPassword() = requireStore().createPassword()
fun navigateTo(file: File) {
requireStore().clearSearch()
model.navigateTo(
file,
recyclerViewState = binding.passRecycler.layoutManager!!.onSaveInstanceState()
)
requireStore().supportActionBar?.setDisplayHomeAsUpEnabled(true)
}
fun scrollToOnNextRefresh(file: File) {
scrollTarget = file
}
interface OnFragmentInteractionListener {
fun onFragmentInteraction(item: PasswordItem)
}

View file

@ -67,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.PreferenceKeys
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.contains
import com.zeapo.pwdstore.utils.isInsideRepository
import com.zeapo.pwdstore.utils.listFilesRecursively
import com.zeapo.pwdstore.utils.requestInputFocusOnView
@ -734,10 +735,17 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
/**
* Refreshes the password list by re-executing the last navigation or search action, preserving
* the navigation stack and scroll position. If the current directory no longer exists,
* navigation is reset to the repository root.
* navigation is reset to the repository root. If the optional [target] argument is provided,
* it will be entered if it is a directory or scrolled into view if it is a file (both inside
* the current directory).
*/
fun refreshPasswordList() {
if (model.currentDir.value?.isDirectory == true) {
fun refreshPasswordList(target: File? = null) {
if (target?.isDirectory == true && model.currentDir.value?.contains(target) == true) {
plist?.navigateTo(target)
} else if (target?.isFile == true && model.currentDir.value?.contains(target) == true) {
// Creating new passwords is handled by an activity, so we will refresh in onStart.
plist?.scrollToOnNextRefresh(target)
} else if (model.currentDir.value?.isDirectory == true) {
model.forceRefresh()
} else {
model.reset()
@ -764,7 +772,8 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
}
REQUEST_CODE_ENCRYPT -> {
commitChange(resources.getString(R.string.git_commit_add_text,
data!!.extras!!.getString("LONG_NAME")))
data!!.extras!!.getString(PasswordCreationActivity.RETURN_EXTRA_LONG_NAME)))
refreshPasswordList(File(data.extras!!.getString(PasswordCreationActivity.RETURN_EXTRA_CREATED_FILE)!!))
}
BaseGitActivity.REQUEST_INIT, NEW_REPO_BUTTON -> initializeRepositoryInfo()
BaseGitActivity.REQUEST_SYNC, BaseGitActivity.REQUEST_PULL -> refreshPasswordList()

View file

@ -447,6 +447,8 @@ open class SearchableRepositoryAdapter<T : RecyclerView.ViewHolder>(
return selectedFiles.map { it.toPasswordItem(root) }
}
fun getPositionForFile(file: File) = itemKeyProvider.getPosition(file.absolutePath)
final override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): T {
val view = LayoutInflater.from(parent.context)
.inflate(layoutRes, parent, false)

View file

@ -36,8 +36,9 @@ class FolderCreationDialogFragment : DialogFragment() {
val dialog = requireDialog()
val materialTextView = dialog.findViewById<TextInputEditText>(R.id.folder_name_text)
val folderName = materialTextView.text.toString()
File("$currentDir/$folderName").mkdir()
(requireActivity() as PasswordStore).refreshPasswordList()
val newFolder = File("$currentDir/$folderName")
newFolder.mkdir()
(requireActivity() as PasswordStore).refreshPasswordList(newFolder)
dismiss()
}

View file

@ -59,6 +59,23 @@ fun Activity.snackbar(
fun File.listFilesRecursively() = walkTopDown().filter { !it.isDirectory }.toList()
/**
* Checks whether this [File] is a directory that contains [other].
*/
fun File.contains(other: File): Boolean {
if (!isDirectory)
return false
if (!other.exists())
return false
val relativePath = try {
other.relativeTo(this)
} catch (e: Exception) {
return false
}
// Direct containment is equivalent to the relative path being equal to the filename.
return relativePath.path == other.name
}
fun Context.resolveAttribute(attr: Int): Int {
val typedValue = TypedValue()
this.theme.resolveAttribute(attr, typedValue, true)