aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/FolderPicker.kt126
-rw-r--r--app/src/main/java/app/passwordstore/ui/list/ListDestination.kt56
-rw-r--r--app/src/main/res/values-it/strings.xml4
-rw-r--r--app/src/main/res/values/strings.xml4
4 files changed, 161 insertions, 29 deletions
diff --git a/app/src/main/java/app/passwordstore/ui/list/FolderPicker.kt b/app/src/main/java/app/passwordstore/ui/list/FolderPicker.kt
new file mode 100644
index 00000000..5b610ff7
--- /dev/null
+++ b/app/src/main/java/app/passwordstore/ui/list/FolderPicker.kt
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2026 Danilo M. <danix@danix.xyz>
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package app.passwordstore.ui.list
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import app.passwordstore.R
+import app.passwordstore.ui.compose.BreadcrumbBar
+import app.passwordstore.ui.compose.FolderRow
+import app.passwordstore.ui.compose.crumbsFor
+import app.passwordstore.ui.compose.theme.SpacingMedium
+import app.passwordstore.util.extensions.sharedPrefs
+import app.passwordstore.util.settings.PreferenceKeys
+import java.io.File
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+
+/**
+ * Picks a folder of the store at [root], starting at [start] (relative to the root, "" is the
+ * root): breadcrumbs jump up, rows open subfolders, "New folder" creates one inside the folder
+ * shown (the list's validation) and opens it, Select reports the folder shown, relative to the
+ * root. Used by the list's move action and by the editor. Shows no secrets; its window inherits
+ * FLAG_SECURE from the screen below.
+ */
+@Composable
+fun FolderPicker(
+ root: File,
+ start: String,
+ onPick: (relativePath: String) -> Unit,
+ onDismiss: () -> Unit,
+ modifier: Modifier = Modifier,
+) {
+ val context = LocalContext.current
+ var path by rememberSaveable { mutableStateOf(start) }
+ var naming by rememberSaveable { mutableStateOf(false) }
+ val showHidden = remember {
+ context.sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
+ }
+ val rows by
+ produceState<ImmutableList<ListRow>>(persistentListOf(), path, naming) {
+ val dir = File(root, path)
+ // ponytail: a composable has no injected DispatcherProvider; plain IO for a directory read.
+ @Suppress("RawDispatchersUse")
+ value = withContext(Dispatchers.IO) { listRows(subfolders(dir, showHidden), dir).folders }
+ }
+ if (naming) {
+ // The list's own dialog and checks: blank, already a file or folder, outside the store
+ // (component-wise isInsideRepository).
+ val dir = File(root, path)
+ FolderNameDialog(
+ title = stringResource(R.string.title_create_folder),
+ message = null,
+ confirm = stringResource(R.string.button_create),
+ validate = { folderNameError(it, File(dir, it)) },
+ onConfirm = { name ->
+ val folder = File(dir, name).canonicalFile
+ folder.mkdirs()
+ naming = false
+ path = folder.relativeTo(root.canonicalFile).path
+ },
+ onDismiss = { naming = false },
+ )
+ }
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ modifier = modifier,
+ title = { Text(stringResource(R.string.folder_picker_title)) },
+ text = {
+ Column {
+ BreadcrumbBar(
+ crumbsFor(path),
+ stringResource(R.string.list_store_root),
+ onCrumbClick = { path = it },
+ modifier = Modifier.fillMaxWidth().padding(bottom = SpacingMedium),
+ )
+ // ponytail: a plain scrolling column, fine for the folders of one level; a LazyColumn
+ // if a store ever has thousands of subfolders in one folder.
+ Column(Modifier.heightIn(max = 360.dp).verticalScroll(rememberScrollState())) {
+ rows.forEach { row ->
+ key(row.path) {
+ FolderRow(
+ name = row.label,
+ count = row.count,
+ onClick = { path = File(row.path).relativeTo(root).path },
+ )
+ }
+ }
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(onClick = { onPick(path) }) { Text(stringResource(R.string.crypto_select)) }
+ },
+ dismissButton = {
+ Row {
+ TextButton(onClick = { naming = true }) { Text(stringResource(R.string.folder_picker_new)) }
+ TextButton(onClick = onDismiss) { Text(stringResource(R.string.dialog_cancel)) }
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
index 98c820b1..147e77b7 100644
--- a/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
+++ b/app/src/main/java/app/passwordstore/ui/list/ListDestination.kt
@@ -5,11 +5,7 @@
package app.passwordstore.ui.list
-import android.app.Activity
-import android.content.Intent
import androidx.activity.compose.BackHandler
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
@@ -37,7 +33,6 @@ import androidx.lifecycle.lifecycleScope
import app.passwordstore.R
import app.passwordstore.data.repo.PasswordRepository
import app.passwordstore.ui.compose.theme.SpacingMedium
-import app.passwordstore.ui.folderselect.SelectFolderActivity
import app.passwordstore.ui.git.base.BaseGitActivity.GitOp
import app.passwordstore.ui.git.config.GitServerConfigActivity
import app.passwordstore.ui.main.MainActivity
@@ -55,14 +50,15 @@ private enum class ListDialog {
NewFolder,
Rename,
Delete,
+ Move,
Clone,
NotInitialized,
}
/**
* The list at [path] (relative to the store root): wires [ListScreen] to the [viewModel], the
- * legacy activities and the store operations. [onOpenFolder] takes a path relative to the root,
- * [onOpenEntry] an entry file.
+ * folder picker, the legacy activities and the store operations. [onOpenFolder] takes a path
+ * relative to the root, [onOpenEntry] an entry file.
*/
@Composable
fun ListDestination(
@@ -106,20 +102,6 @@ fun ListDestination(
BackHandler(enabled = selected.isNotEmpty()) { selected = emptyList() }
BackHandler(enabled = selected.isEmpty() && query.isNotEmpty()) { query = "" }
- val moveLauncher =
- rememberLauncherForActivityResult(StartActivityForResult()) { result ->
- val data = result.data
- val target = data?.getStringExtra("SELECTED_FOLDER_PATH")?.let(::File)
- val files = data?.getStringArrayExtra("Files")?.map(::File)
- if (result.resultCode == Activity.RESULT_OK && target?.isDirectory == true && files != null) {
- selected = emptyList()
- activity.lifecycleScope.launch {
- activity.moveItems(files, target)
- refresh()
- }
- }
- }
-
fun ifInitialized(action: () -> Unit) {
if (PasswordRepository.isInitialized) action() else dialog = ListDialog.NotInitialized
}
@@ -166,12 +148,7 @@ fun ListDestination(
onGitOp = { op -> ifInitialized { runGit(op) } },
onSettings = activity::openSettings,
onNew = { dialog = ListDialog.Create },
- onMove = {
- moveLauncher.launch(
- Intent(activity, SelectFolderActivity::class.java)
- .putExtra("Files", selected.toTypedArray())
- )
- },
+ onMove = { dialog = ListDialog.Move },
onDelete = {
val files = selected.map(::File)
val count = countFiles(files)
@@ -254,6 +231,24 @@ fun ListDestination(
onDismiss = { dialog = ListDialog.None },
dismiss = stringResource(R.string.dialog_no),
)
+ ListDialog.Move ->
+ FolderPicker(
+ root = root,
+ start = path,
+ onPick = { target ->
+ // One move per pick: a double tap on Select must not move twice.
+ if (dialog == ListDialog.Move) {
+ val files = selected.map(::File)
+ dialog = ListDialog.None
+ selected = emptyList()
+ activity.lifecycleScope.launch {
+ activity.moveItems(files, File(root, target))
+ refresh()
+ }
+ }
+ },
+ onDismiss = { dialog = ListDialog.None },
+ )
ListDialog.Clone ->
Prompt(
text = stringResource(R.string.clone_git_repo),
@@ -276,9 +271,12 @@ fun ListDestination(
}
}
-/** Asks for a folder name; [validate] returns an error string resource or null. */
+/**
+ * Asks for a folder name; [validate] returns an error string resource or null. Shared with the
+ * folder picker's "New folder".
+ */
@Composable
-private fun FolderNameDialog(
+internal fun FolderNameDialog(
title: String,
message: String?,
confirm: String,
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 31c34b4d..17e149af 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -358,4 +358,8 @@
<string name="entry_show_password">Mostra password</string>
<string name="entry_hide_password">Nascondi password</string>
<string name="entry_copy_name">Copia nome</string>
+
+ <!-- Compose folder picker -->
+ <string name="folder_picker_title">Scegli una cartella</string>
+ <string name="folder_picker_new">Nuova cartella</string>
</resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2e208158..5370751e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -392,4 +392,8 @@
<string name="entry_show_password">Show password</string>
<string name="entry_hide_password">Hide password</string>
<string name="entry_copy_name">Copy name</string>
+
+ <!-- Compose folder picker -->
+ <string name="folder_picker_title">Choose a folder</string>
+ <string name="folder_picker_new">New folder</string>
</resources>