mirror of
https://github.com/android-password-store/Android-Password-Store.git
synced 2025-09-07 16:09:38 +02:00
feat(app): support toggling read-only status in DecryptScreen
This commit is contained in:
parent
6d73b63435
commit
c866bb9fb1
|
@ -19,6 +19,8 @@ import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.capitalize
|
||||||
|
import androidx.compose.ui.text.intl.Locale
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import app.passwordstore.R
|
import app.passwordstore.R
|
||||||
|
@ -32,11 +34,23 @@ import kotlin.time.ExperimentalTime
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composable to show a [PasswordEntry]. It can be used for both read-only usage (decrypt screen) or
|
||||||
|
* read-write (encrypt screen) to allow sharing UI logic for both these screens and deferring all
|
||||||
|
* the cryptographic aspects to its parent.
|
||||||
|
*
|
||||||
|
* When [readOnly] is `true`, the Composable assumes that we're showcasing the provided [entry] to
|
||||||
|
* the user and does not offer any edit capabilities.
|
||||||
|
*
|
||||||
|
* When [readOnly] is `false`, the [TextField]s are rendered editable but currently do not pass up
|
||||||
|
* their "updated" state to anything. This will be changed in later commits.
|
||||||
|
*/
|
||||||
@OptIn(ExperimentalTime::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalTime::class, ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun PasswordEntryScreen(
|
fun PasswordEntryScreen(
|
||||||
entryName: String,
|
entryName: String,
|
||||||
entry: PasswordEntry,
|
entry: PasswordEntry,
|
||||||
|
readOnly: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
@ -61,21 +75,22 @@ fun PasswordEntryScreen(
|
||||||
value = entry.password!!,
|
value = entry.password!!,
|
||||||
label = "Password",
|
label = "Password",
|
||||||
initialVisibility = false,
|
initialVisibility = false,
|
||||||
|
readOnly = readOnly,
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (entry.hasTotp()) {
|
if (entry.hasTotp() && readOnly) {
|
||||||
val totp by entry.totp.collectAsState(runBlocking { entry.totp.first() })
|
val totp by entry.totp.collectAsState(runBlocking { entry.totp.first() })
|
||||||
TextField(
|
TextField(
|
||||||
value = totp.value,
|
value = totp.value,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = readOnly,
|
||||||
label = { Text("OTP (expires in ${totp.remainingTime.inWholeSeconds}s)") },
|
label = { Text("OTP (expires in ${totp.remainingTime.inWholeSeconds}s)") },
|
||||||
trailingIcon = { CopyButton({ totp.value }) },
|
trailingIcon = { CopyButton({ totp.value }) },
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (entry.username != null) {
|
if (entry.username != null && readOnly) {
|
||||||
TextField(
|
TextField(
|
||||||
value = entry.username!!,
|
value = entry.username!!,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
|
@ -85,11 +100,41 @@ fun PasswordEntryScreen(
|
||||||
modifier = Modifier.padding(bottom = 8.dp),
|
modifier = Modifier.padding(bottom = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ExtraContent(entry = entry, readOnly = readOnly)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
private fun ExtraContent(
|
||||||
|
entry: PasswordEntry,
|
||||||
|
readOnly: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
if (readOnly) {
|
||||||
|
entry.extraContent.forEach { (label, value) ->
|
||||||
|
TextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = true,
|
||||||
|
label = { Text(label.capitalize(Locale.current)) },
|
||||||
|
trailingIcon = { CopyButton({ value }) },
|
||||||
|
modifier = modifier.padding(bottom = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
TextField(
|
||||||
|
value = entry.extraContentWithoutAuthData,
|
||||||
|
onValueChange = {},
|
||||||
|
readOnly = false,
|
||||||
|
label = { Text("Extra content") },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CopyButton(
|
private fun CopyButton(
|
||||||
textToCopy: () -> String,
|
textToCopy: () -> String,
|
||||||
|
@ -110,7 +155,9 @@ private fun CopyButton(
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun PasswordEntryPreview() {
|
private fun PasswordEntryPreview() {
|
||||||
APSThemePreview { PasswordEntryScreen("Test Entry", createTestEntry()) }
|
APSThemePreview {
|
||||||
|
PasswordEntryScreen(entryName = "Test Entry", entry = createTestEntry(), readOnly = true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTestEntry() =
|
private fun createTestEntry() =
|
||||||
|
@ -121,6 +168,7 @@ private fun createTestEntry() =
|
||||||
|My Password
|
|My Password
|
||||||
|otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
|
|otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30
|
||||||
|login: msfjarvis
|
|login: msfjarvis
|
||||||
|
|URL: example.com
|
||||||
"""
|
"""
|
||||||
.trimMargin()
|
.trimMargin()
|
||||||
.encodeToByteArray()
|
.encodeToByteArray()
|
||||||
|
|
|
@ -21,13 +21,14 @@ public fun PasswordField(
|
||||||
value: String,
|
value: String,
|
||||||
label: String,
|
label: String,
|
||||||
initialVisibility: Boolean,
|
initialVisibility: Boolean,
|
||||||
|
readOnly: Boolean,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var visible by remember { mutableStateOf(initialVisibility) }
|
var visible by remember { mutableStateOf(initialVisibility) }
|
||||||
TextField(
|
TextField(
|
||||||
value = value,
|
value = value,
|
||||||
onValueChange = {},
|
onValueChange = {},
|
||||||
readOnly = true,
|
readOnly = readOnly,
|
||||||
label = { Text(label) },
|
label = { Text(label) },
|
||||||
visualTransformation =
|
visualTransformation =
|
||||||
if (visible) VisualTransformation.None else PasswordVisualTransformation(),
|
if (visible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
|
|
Loading…
Reference in a new issue