mirror of
https://github.com/android-password-store/Android-Password-Store
synced 2025-08-31 14:25:28 +00:00
Refactor navigation and search code (#2134)
This commit is contained in:
@@ -21,7 +21,7 @@ import androidx.core.text.bold
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.underline
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import app.passwordstore.R
|
||||
@@ -38,6 +38,8 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryAdapter
|
||||
import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
|
||||
import com.github.androidpasswordstore.autofillparser.FormOrigin
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import logcat.LogPriority.ERROR
|
||||
import logcat.logcat
|
||||
|
||||
@@ -85,9 +87,7 @@ class AutofillFilterView : AppCompatActivity() {
|
||||
private lateinit var directoryStructure: DirectoryStructure
|
||||
private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate)
|
||||
|
||||
private val model: SearchableRepositoryViewModel by viewModels {
|
||||
ViewModelProvider.AndroidViewModelFactory(application)
|
||||
}
|
||||
private val model: SearchableRepositoryViewModel by viewModels()
|
||||
|
||||
private val decryptAction =
|
||||
registerForActivityResult(StartActivityForResult()) { result ->
|
||||
@@ -193,20 +193,23 @@ class AutofillFilterView : AppCompatActivity() {
|
||||
R.string.oreo_autofill_match_with,
|
||||
formOrigin.getPrettyIdentifier(applicationContext)
|
||||
)
|
||||
model.searchResult.observe(this@AutofillFilterView) { result ->
|
||||
val list = result.passwordItems
|
||||
(rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
|
||||
rvPassword.scrollToPosition(0)
|
||||
model.searchResult
|
||||
.flowWithLifecycle(lifecycle)
|
||||
.onEach { result ->
|
||||
val list = result.passwordItems
|
||||
(rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) {
|
||||
rvPassword.scrollToPosition(0)
|
||||
}
|
||||
// Switch RecyclerView out for a "no results" message if the new list is empty and
|
||||
// the message is not yet shown (and vice versa).
|
||||
if (
|
||||
(list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
|
||||
(list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id)
|
||||
) {
|
||||
rvPasswordSwitcher.showNext()
|
||||
}
|
||||
}
|
||||
// Switch RecyclerView out for a "no results" message if the new list is empty and
|
||||
// the message is not yet shown (and vice versa).
|
||||
if (
|
||||
(list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) ||
|
||||
(list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id)
|
||||
) {
|
||||
rvPasswordSwitcher.showNext()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -10,6 +10,7 @@ import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import app.passwordstore.R
|
||||
@@ -23,6 +24,8 @@ import app.passwordstore.util.viewmodel.SearchableRepositoryViewModel
|
||||
import com.github.michaelbull.result.onFailure
|
||||
import com.github.michaelbull.result.runCatching
|
||||
import java.io.File
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
||||
class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
||||
@@ -51,9 +54,10 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
||||
|
||||
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
|
||||
model.navigateTo(File(path), listMode = ListMode.DirectoriesOnly, pushPreviousLocation = false)
|
||||
model.searchResult.observe(viewLifecycleOwner) { result ->
|
||||
recyclerAdapter.submitList(result.passwordItems)
|
||||
}
|
||||
model.searchResult
|
||||
.flowWithLifecycle(lifecycle)
|
||||
.onEach { result -> recyclerAdapter.submitList(result.passwordItems) }
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
@@ -77,7 +81,7 @@ class SelectFolderFragment : Fragment(R.layout.password_recycler_view) {
|
||||
}
|
||||
|
||||
val currentDir: File
|
||||
get() = model.currentDir.value!!
|
||||
get() = model.currentDir.value
|
||||
|
||||
interface OnFragmentInteractionListener {
|
||||
|
||||
|
@@ -18,6 +18,7 @@ import androidx.appcompat.view.ActionMode
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -48,6 +49,8 @@ import com.github.michaelbull.result.runCatching
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import me.zhanghai.android.fastscroll.FastScrollerBuilder
|
||||
|
||||
@@ -74,7 +77,7 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
||||
}
|
||||
|
||||
val currentDir: File
|
||||
get() = model.currentDir.value!!
|
||||
get() = model.currentDir.value
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
@@ -174,36 +177,37 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
|
||||
|
||||
val path = requireNotNull(requireArguments().getString(PasswordStore.REQUEST_ARG_PATH))
|
||||
model.navigateTo(File(path), pushPreviousLocation = false)
|
||||
model.searchResult.observe(viewLifecycleOwner) { result ->
|
||||
// Only run animations when the new list is filtered, i.e., the user submitted a search,
|
||||
// and not on folder navigations since the latter leads to too many removal animations.
|
||||
(recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered
|
||||
recyclerAdapter.submitList(result.passwordItems) {
|
||||
when {
|
||||
result.isFiltered -> {
|
||||
// When the result is filtered, we always scroll to the top since that is
|
||||
// where
|
||||
// the best fuzzy match appears.
|
||||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
scrollTarget != null -> {
|
||||
scrollTarget?.let {
|
||||
recyclerView.scrollToPosition(recyclerAdapter.getPositionForFile(it))
|
||||
model.searchResult
|
||||
.flowWithLifecycle(lifecycle)
|
||||
.onEach { result ->
|
||||
// Only run animations when the new list is filtered, i.e., the user submitted a search,
|
||||
// and not on folder navigation since the latter leads to too many removal animations.
|
||||
(recyclerView.itemAnimator as OnOffItemAnimator).isEnabled = result.isFiltered
|
||||
recyclerAdapter.submitList(result.passwordItems) {
|
||||
when {
|
||||
result.isFiltered -> {
|
||||
// When the result is filtered, we always scroll to the top since that is
|
||||
// where the best fuzzy match appears.
|
||||
recyclerView.scrollToPosition(0)
|
||||
}
|
||||
scrollTarget = null
|
||||
}
|
||||
else -> {
|
||||
// When the result is not filtered and there is a saved scroll position for
|
||||
// it,
|
||||
// we try to restore it.
|
||||
recyclerViewStateToRestore?.let {
|
||||
recyclerView.layoutManager!!.onRestoreInstanceState(it)
|
||||
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.
|
||||
recyclerViewStateToRestore?.let {
|
||||
recyclerView.layoutManager!!.onRestoreInstanceState(it)
|
||||
}
|
||||
recyclerViewStateToRestore = null
|
||||
}
|
||||
recyclerViewStateToRestore = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
}
|
||||
|
||||
private val actionModeCallback =
|
||||
|
@@ -20,7 +20,7 @@ import androidx.appcompat.widget.SearchView.OnQueryTextListener
|
||||
import androidx.core.content.edit
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import app.passwordstore.R
|
||||
import app.passwordstore.data.password.PasswordItem
|
||||
@@ -71,9 +71,7 @@ class PasswordStore : BaseGitActivity() {
|
||||
private lateinit var searchItem: MenuItem
|
||||
private val settings by lazy { sharedPrefs }
|
||||
|
||||
private val model: SearchableRepositoryViewModel by viewModels {
|
||||
ViewModelProvider.AndroidViewModelFactory(application)
|
||||
}
|
||||
private val model: SearchableRepositoryViewModel by viewModels()
|
||||
|
||||
private val listRefreshAction =
|
||||
registerForActivityResult(StartActivityForResult()) { result ->
|
||||
@@ -186,10 +184,12 @@ class PasswordStore : BaseGitActivity() {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_pwdstore)
|
||||
|
||||
model.currentDir.observe(this) { dir ->
|
||||
val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
|
||||
supportActionBar?.apply {
|
||||
if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
|
||||
lifecycleScope.launchWhenCreated {
|
||||
model.currentDir.flowWithLifecycle(lifecycle).collect { dir ->
|
||||
val basePath = PasswordRepository.getRepositoryDirectory().absoluteFile
|
||||
supportActionBar?.apply {
|
||||
if (dir != basePath) title = dir.name else setTitle(R.string.app_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +238,7 @@ class PasswordStore : BaseGitActivity() {
|
||||
// List the contents of the current directory if the user enters a blank
|
||||
// search term.
|
||||
if (filter.isEmpty())
|
||||
model.navigateTo(newDirectory = model.currentDir.value!!, pushPreviousLocation = false)
|
||||
model.navigateTo(newDirectory = model.currentDir.value, pushPreviousLocation = false)
|
||||
else model.search(filter)
|
||||
return true
|
||||
}
|
||||
@@ -544,12 +544,12 @@ class PasswordStore : BaseGitActivity() {
|
||||
*/
|
||||
fun refreshPasswordList(target: File? = null) {
|
||||
val plist = getPasswordFragment()
|
||||
if (target?.isDirectory == true && model.currentDir.value?.contains(target) == true) {
|
||||
if (target?.isDirectory == true && model.currentDir.value.contains(target)) {
|
||||
plist?.navigateTo(target)
|
||||
} else if (target?.isFile == true && model.currentDir.value?.contains(target) == true) {
|
||||
} else if (target?.isFile == true && model.currentDir.value.contains(target)) {
|
||||
// Creating new passwords is handled by an activity, so we will refresh in onStart.
|
||||
plist?.scrollToOnNextRefresh(target)
|
||||
} else if (model.currentDir.value?.isDirectory == true) {
|
||||
} else if (model.currentDir.value.isDirectory) {
|
||||
model.forceRefresh()
|
||||
} else {
|
||||
model.reset()
|
||||
|
39
app/src/main/java/app/passwordstore/util/Perf.kt
Normal file
39
app/src/main/java/app/passwordstore/util/Perf.kt
Normal file
@@ -0,0 +1,39 @@
|
||||
// It's okay if this stays unused for the most part since it is development tooling.
|
||||
@file:Suppress("Unused")
|
||||
|
||||
package app.passwordstore.util
|
||||
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import logcat.logcat
|
||||
|
||||
/**
|
||||
* Small helper to execute a given [block] and log the time it took to execute it. Intended for use
|
||||
* in day-to-day perf investigations and code using it should probably not be shipped.
|
||||
*/
|
||||
suspend fun <T> logExecutionTime(tag: String, block: suspend () -> T): T {
|
||||
val start = SystemClock.uptimeMillis()
|
||||
val res = block()
|
||||
val end = SystemClock.uptimeMillis()
|
||||
logcat(tag) { "Finished in ${end - start}ms" }
|
||||
return res
|
||||
}
|
||||
|
||||
fun <T> logExecutionTimeBlocking(tag: String, block: () -> T): T {
|
||||
val start = SystemClock.uptimeMillis()
|
||||
val res = block()
|
||||
val end = SystemClock.uptimeMillis()
|
||||
logcat(tag) { "Finished in ${end - start}ms" }
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws if called on the main thread, used to ensure an operation being offloaded to a background
|
||||
* thread is correctly being moved off the main thread.
|
||||
*/
|
||||
@Suppress("NOTHING_TO_INLINE")
|
||||
inline fun checkMainThread() {
|
||||
require(Looper.myLooper() != Looper.getMainLooper()) {
|
||||
"This operation must not run on the main thread"
|
||||
}
|
||||
}
|
@@ -6,7 +6,6 @@ package app.passwordstore.util.extensions
|
||||
|
||||
import app.passwordstore.data.repo.PasswordRepository
|
||||
import com.github.michaelbull.result.Err
|
||||
import com.github.michaelbull.result.Ok
|
||||
import com.github.michaelbull.result.Result
|
||||
import com.github.michaelbull.result.getOrElse
|
||||
import com.github.michaelbull.result.runCatching
|
||||
@@ -16,14 +15,6 @@ import logcat.asLog
|
||||
import org.eclipse.jgit.lib.ObjectId
|
||||
import org.eclipse.jgit.revwalk.RevCommit
|
||||
|
||||
/** The default OpenPGP provider for the app */
|
||||
const val OPENPGP_PROVIDER = "org.sufficientlysecure.keychain"
|
||||
|
||||
/** Clears the given [flag] from the value of this [Int] */
|
||||
fun Int.clearFlag(flag: Int): Int {
|
||||
return this and flag.inv()
|
||||
}
|
||||
|
||||
/** Checks if this [Int] contains the given [flag] */
|
||||
infix fun Int.hasFlag(flag: Int): Boolean {
|
||||
return this and flag == flag
|
||||
@@ -73,25 +64,12 @@ val RevCommit.time: Date
|
||||
return Date(epochMilliseconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits this [String] into an [Array] of [String] s, split on the UNIX LF line ending and stripped
|
||||
* of any empty lines.
|
||||
*/
|
||||
fun String.splitLines(): Array<String> {
|
||||
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
}
|
||||
|
||||
/** Alias to [lazy] with thread safety mode always set to [LazyThreadSafetyMode.NONE]. */
|
||||
fun <T> unsafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() }
|
||||
|
||||
/** A convenience extension to turn a [Throwable] with a message into a loggable string. */
|
||||
fun Throwable.asLog(message: String): String = "$message\n${asLog()}"
|
||||
|
||||
/** Extension on [Result] that returns if the type is [Ok] */
|
||||
fun <V, E> Result<V, E>.isOk(): Boolean {
|
||||
return this is Ok<V>
|
||||
}
|
||||
|
||||
/** Extension on [Result] that returns if the type is [Err] */
|
||||
fun <V, E> Result<V, E>.isErr(): Boolean {
|
||||
return this is Err<E>
|
||||
|
@@ -5,16 +5,13 @@
|
||||
package app.passwordstore.util.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Parcelable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.recyclerview.selection.ItemDetailsLookup
|
||||
import androidx.recyclerview.selection.ItemKeyProvider
|
||||
import androidx.recyclerview.selection.Selection
|
||||
@@ -26,30 +23,35 @@ import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import app.passwordstore.data.password.PasswordItem
|
||||
import app.passwordstore.data.repo.PasswordRepository
|
||||
import app.passwordstore.injection.prefs.SettingsPreferences
|
||||
import app.passwordstore.util.autofill.AutofillPreferences
|
||||
import app.passwordstore.util.autofill.DirectoryStructure
|
||||
import app.passwordstore.util.extensions.sharedPrefs
|
||||
import app.passwordstore.util.extensions.unsafeLazy
|
||||
import app.passwordstore.util.checkMainThread
|
||||
import app.passwordstore.util.coroutines.DispatcherProvider
|
||||
import app.passwordstore.util.settings.PasswordSortOrder
|
||||
import app.passwordstore.util.settings.PreferenceKeys
|
||||
import com.github.androidpasswordstore.sublimefuzzy.Fuzzy
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import java.io.File
|
||||
import java.text.Collator
|
||||
import java.util.Locale
|
||||
import java.util.Stack
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.yield
|
||||
import me.zhanghai.android.fastscroll.PopupTextProvider
|
||||
@@ -108,8 +110,15 @@ enum class ListMode {
|
||||
AllEntries
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@HiltViewModel
|
||||
class SearchableRepositoryViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
application: Application,
|
||||
dispatcherProvider: DispatcherProvider,
|
||||
@SettingsPreferences private val settings: SharedPreferences,
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private var _updateCounter = 0
|
||||
private val updateCounter: Int
|
||||
@@ -121,7 +130,6 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
|
||||
private val root
|
||||
get() = PasswordRepository.getRepositoryDirectory()
|
||||
private val settings by unsafeLazy { application.sharedPrefs }
|
||||
private val showHiddenContents
|
||||
get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
|
||||
private val defaultSearchMode
|
||||
@@ -169,8 +177,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
|
||||
private fun updateSearchAction(action: SearchAction) = action.copy(updateCounter = updateCounter)
|
||||
|
||||
private val searchAction =
|
||||
MutableLiveData(
|
||||
private val searchActionFlow =
|
||||
MutableStateFlow(
|
||||
makeSearchAction(
|
||||
baseDirectory = root,
|
||||
filter = "",
|
||||
@@ -179,13 +187,13 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
listMode = ListMode.AllEntries
|
||||
)
|
||||
)
|
||||
private val searchActionFlow = searchAction.asFlow().distinctUntilChanged()
|
||||
|
||||
data class SearchResult(val passwordItems: List<PasswordItem>, val isFiltered: Boolean)
|
||||
|
||||
val searchResult =
|
||||
searchActionFlow
|
||||
.mapLatest { searchAction ->
|
||||
checkMainThread()
|
||||
val listResultFlow =
|
||||
when (searchAction.searchMode) {
|
||||
SearchMode.RecursivelyInSubdirectories ->
|
||||
@@ -194,14 +202,20 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
}
|
||||
val prefilteredResultFlow =
|
||||
when (searchAction.listMode) {
|
||||
ListMode.FilesOnly -> listResultFlow.filter { it.isFile }
|
||||
ListMode.DirectoriesOnly -> listResultFlow.filter { it.isDirectory }
|
||||
ListMode.FilesOnly ->
|
||||
listResultFlow.filter { it.isFile }.flowOn(dispatcherProvider.io())
|
||||
ListMode.DirectoriesOnly ->
|
||||
listResultFlow.filter { it.isDirectory }.flowOn(dispatcherProvider.io())
|
||||
ListMode.AllEntries -> listResultFlow
|
||||
}
|
||||
val passwordList =
|
||||
when (if (searchAction.filter == "") FilterMode.NoFilter else searchAction.filterMode) {
|
||||
FilterMode.NoFilter -> {
|
||||
prefilteredResultFlow.map { it.toPasswordItem() }.toList().sortedWith(itemComparator)
|
||||
prefilteredResultFlow
|
||||
.map { it.toPasswordItem() }
|
||||
.flowOn(dispatcherProvider.io())
|
||||
.toList()
|
||||
.sortedWith(itemComparator)
|
||||
}
|
||||
FilterMode.StrictDomain -> {
|
||||
check(searchAction.listMode == ListMode.FilesOnly) {
|
||||
@@ -214,6 +228,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
regex.containsMatchIn(absoluteFile.relativeTo(root).path)
|
||||
}
|
||||
.map { it.toPasswordItem() }
|
||||
.flowOn(dispatcherProvider.io())
|
||||
.toList()
|
||||
.sortedWith(itemComparator)
|
||||
} else {
|
||||
@@ -227,6 +242,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
Pair(item.fuzzyMatch(searchAction.filter), item)
|
||||
}
|
||||
.filter { it.first > 0 }
|
||||
.flowOn(dispatcherProvider.io())
|
||||
.toList()
|
||||
.sortedWith(
|
||||
compareByDescending<Pair<Int, PasswordItem>> { it.first }
|
||||
@@ -237,7 +253,7 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
}
|
||||
SearchResult(passwordList, isFiltered = searchAction.filterMode != FilterMode.NoFilter)
|
||||
}
|
||||
.asLiveData(Dispatchers.IO)
|
||||
.flowOn(dispatcherProvider.io())
|
||||
|
||||
private fun shouldTake(file: File) =
|
||||
with(file) {
|
||||
@@ -270,8 +286,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
.filter(::shouldTake)
|
||||
}
|
||||
|
||||
private val _currentDir = MutableLiveData(root)
|
||||
val currentDir = _currentDir as LiveData<File>
|
||||
private val _currentDir = MutableStateFlow(root)
|
||||
val currentDir = _currentDir.asStateFlow()
|
||||
|
||||
data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
|
||||
|
||||
@@ -286,9 +302,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
if (!newDirectory.exists()) return
|
||||
require(newDirectory.isDirectory) { "Can only navigate to a directory" }
|
||||
if (pushPreviousLocation) {
|
||||
navigationStack.push(NavigationStackEntry(_currentDir.value!!, recyclerViewState))
|
||||
navigationStack.push(NavigationStackEntry(_currentDir.value, recyclerViewState))
|
||||
}
|
||||
searchAction.postValue(
|
||||
searchActionFlow.update {
|
||||
makeSearchAction(
|
||||
filter = "",
|
||||
baseDirectory = newDirectory,
|
||||
@@ -296,8 +312,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
searchMode = SearchMode.InCurrentDirectoryOnly,
|
||||
listMode = listMode
|
||||
)
|
||||
)
|
||||
_currentDir.postValue(newDirectory)
|
||||
}
|
||||
_currentDir.update { newDirectory }
|
||||
}
|
||||
|
||||
val canNavigateBack
|
||||
@@ -330,20 +346,20 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
|
||||
listMode: ListMode = ListMode.AllEntries
|
||||
) {
|
||||
require(baseDirectory?.isDirectory != false) { "Can only search in a directory" }
|
||||
searchAction.postValue(
|
||||
searchActionFlow.update {
|
||||
makeSearchAction(
|
||||
filter = filter,
|
||||
baseDirectory = baseDirectory ?: _currentDir.value!!,
|
||||
baseDirectory = baseDirectory ?: _currentDir.value,
|
||||
filterMode = filterMode,
|
||||
searchMode = searchMode ?: defaultSearchMode,
|
||||
listMode = listMode
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun forceRefresh() {
|
||||
forceUpdateOnNextSearchAction()
|
||||
searchAction.postValue(updateSearchAction(searchAction.value!!))
|
||||
searchActionFlow.update { updateSearchAction(searchActionFlow.value) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
Reference in New Issue
Block a user