From 58bf4426d3dc3db51089bd6fd4af1e382040a5c6 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Mon, 2 Nov 2026 21:25:16 +0100 Subject: feat(app): add the list rows and folder navigation model Co-Authored-By: Claude Opus 5.5 --- .../java/app/passwordstore/ui/list/ListModel.kt | 61 ++++++++++++++++++++++ .../viewmodel/SearchableRepositoryViewModel.kt | 7 +++ 2 files changed, 68 insertions(+) create mode 100644 app/src/main/java/app/passwordstore/ui/list/ListModel.kt (limited to 'app/src/main') 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. + * 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 = persistentListOf(), + val entries: ImmutableList = 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, 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> { + 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() -- cgit v1.2.3