aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-02 21:25:16 +0100
committerDanilo M. <danix@danix.xyz>2026-11-02 21:25:16 +0100
commit58bf4426d3dc3db51089bd6fd4af1e382040a5c6 (patch)
tree32f623895dca82ba4f842c07f89d13448572eb60 /app/src/main
parenta08ced744fbd7966f58ffa32da6d37bebf166619 (diff)
downloadHardenedPass-58bf4426d3dc3db51089bd6fd4af1e382040a5c6.tar.gz
HardenedPass-58bf4426d3dc3db51089bd6fd4af1e382040a5c6.zip
feat(app): add the list rows and folder navigation model
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/ListModel.kt61
-rw-r--r--app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt7
2 files changed, 68 insertions, 0 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/list/ListModel.kt b/app/src/main/java/app/passwordstore/ui/list/ListModel.kt
new file mode 100644
index 00000000..9154baa5
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/list/ListModel.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.list
+
+import app.passwordstore.ui.compose.crumbsFor
+import java.io.File
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.collections.immutable.toImmutableList
+
+/**
+ * One list row. [path] is the absolute path and the row's key; [count] is a folder's number of
+ * children, 0 for entries.
+ */
+data class ListRow(val path: String, val label: String, val subtitle: String?, val count: Int)
+
+/** The rows of one folder, or of a search inside it: folders above entries. */
+data class ListRows(
+ val folders: ImmutableList<ListRow> = persistentListOf(),
+ val entries: ImmutableList<ListRow> = persistentListOf(),
+)
+
+/**
+ * Splits [files] (already sorted or ranked) into folder and entry rows for [dir], keeping their
+ * order. Labels and subtitles are relative to [dir]: browsing shows plain names, search results
+ * show where they are. Entries get no username or OTP subtitle: both live inside the encrypted
+ * file, and the list never decrypts.
+ */
+fun listRows(files: List<File>, dir: File): ListRows {
+ val (folders, entries) = files.partition { it.isDirectory }
+ return ListRows(
+ folders
+ .map { ListRow(it.absolutePath, it.relativeTo(dir).path, null, childCount(it)) }
+ .toImmutableList(),
+ entries
+ .map {
+ val parent = it.parentFile?.relativeTo(dir)?.path.orEmpty()
+ ListRow(it.absolutePath, it.name.removeSuffix(".gpg"), parent.ifEmpty { null }, 0)
+ }
+ .toImmutableList(),
+ )
+}
+
+/** Children counted for a folder: subfolders and .gpg files, as the old list counted them. */
+private fun childCount(folder: File): Int =
+ folder.listFiles { f -> f.isDirectory || f.extension == "gpg" }?.size ?: 0
+
+/**
+ * How to get from the list at [from] to the list at [to] (both relative to the store root) while
+ * the back stack holds one entry per folder level: how many entries to pop, then which paths to
+ * push. Breadcrumb jumps only pop; opening a folder from search results may pop and push.
+ */
+fun folderMoves(from: String, to: String): Pair<Int, List<String>> {
+ val current = crumbsFor(from)
+ val target = crumbsFor(to)
+ val shared = current.zip(target).takeWhile { (a, b) -> a == b }.size
+ return (current.size - shared) to target.drop(shared).map { it.path }
+}
diff --git a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
index 48804e7a..ca570dc4 100644
--- a/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/app/passwordstore/util/viewmodel/SearchableRepositoryViewModel.kt
@@ -24,6 +24,7 @@ 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.ui.list.listRows
import app.passwordstore.util.autofill.AutofillPreferences
import app.passwordstore.util.checkMainThread
import app.passwordstore.util.coroutines.DispatcherProvider
@@ -302,6 +303,12 @@ constructor(
private val _currentDir = MutableStateFlow(root)
val currentDir = _currentDir.asStateFlow()
+ /** [searchResult] as folder and entry rows for the Compose list, built off the main thread. */
+ val rows =
+ searchResult
+ .map { result -> listRows(result.passwordItems.map { it.file }, currentDir.value) }
+ .flowOn(dispatcherProvider.io())
+
data class NavigationStackEntry(val dir: File, val recyclerViewState: Parcelable?)
private val navigationStack = ArrayDeque<NavigationStackEntry>()