Implement Rust-owned adaptive Android password navigation (#96)
This commit is contained in:
1 parent
36e5f55283
commit
8c9341f214
26 files changed
+1553
-16
No files matched your search
@@ -40,6 +40,9 @@ dependencies {
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.adaptive)
|
||||
implementation(libs.compose.adaptive.layout)
|
||||
implementation(libs.compose.adaptive.navigation)
|
||||
implementation(libs.activity.compose)
|
||||
implementation(libs.lifecycle.compose)
|
||||
implementation(libs.lifecycle.viewmodel)
|
||||
implementation(libs.navigation.runtime)
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# Manual edits can break the build and are not advised.
|
||||
# This file is expected to be part of source control.
|
||||
# To regenerate this file, run: ./gradlew :features:dependencies --write-locks
|
||||
androidx.activity:activity-compose:1.12.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.activity:activity-ktx:1.12.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.activity:activity:1.12.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.activity:activity-compose:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.activity:activity-ktx:1.13.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.activity:activity:1.13.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.annotation:annotation-experimental:1.5.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.annotation:annotation-jvm:1.11.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.annotation:annotation-jvm:1.9.1=debugAndroidTestCompileClasspath,debugCompileClasspath,debugUnitTestCompileClasspath,releaseCompileClasspath
|
||||
@@ -25,6 +25,10 @@ androidx.compose.foundation:foundation-layout-android:1.12.1=debugAndroidTestCom
|
||||
androidx.compose.foundation:foundation-layout:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.foundation:foundation:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3.adaptive:adaptive-android:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3.adaptive:adaptive-layout-android:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3.adaptive:adaptive-layout:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3.adaptive:adaptive-navigation-android:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3.adaptive:adaptive-navigation:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3.adaptive:adaptive:1.3.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3:material3-android:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose.material3:material3:1.4.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
@@ -52,9 +56,9 @@ androidx.compose.ui:ui-util:1.12.1=debugAndroidTestCompileClasspath,debugAndroid
|
||||
androidx.compose.ui:ui:1.12.1=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.compose:compose-bom:2026.09.00=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.concurrent:concurrent-futures:1.1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.core:core-ktx:1.16.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.core:core-ktx:1.18.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.core:core-viewtree:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.core:core:1.16.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.core:core:1.18.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.customview:customview-poolingcontainer:1.0.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.customview:customview:1.0.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
androidx.emoji2:emoji2:1.4.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
@@ -154,7 +158,7 @@ com.google.guava:failureaccess:1.0.2=androidLintTool
|
||||
com.google.guava:failureaccess:1.0.3=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
|
||||
com.google.guava:guava:33.4.0-jre=androidLintTool
|
||||
com.google.guava:guava:33.6.0-jre=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
|
||||
com.google.guava:listenablefuture:1.0=debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
com.google.guava:listenablefuture:1.0=debugAndroidTestCompileClasspath,debugAndroidTestLintChecksClasspath,debugAndroidTestRuntimeClasspath,debugCompileClasspath,debugLintChecksClasspath,debugRuntimeClasspath,debugUnitTestCompileClasspath,debugUnitTestLintChecksClasspath,debugUnitTestRuntimeClasspath,releaseCompileClasspath,releaseLintChecksClasspath,releaseRuntimeClasspath
|
||||
com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,androidLintTool,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
|
||||
com.google.j2objc:j2objc-annotations:3.0.0=androidLintTool
|
||||
com.google.j2objc:j2objc-annotations:3.1=_agp_internal_debug_kspClasspath,_agp_internal_release_kspClasspath,hiltAnnotationProcessorDebugAndroidTest,hiltAnnotationProcessorDebugUnitTest,kspDebugKotlinProcessorClasspath,kspReleaseKotlinProcessorClasspath
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package de.rfc1437.ironstorage.features
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.AnimatedPane
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.heading
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import de.rfc1437.ironstorage.core.PasswordKind
|
||||
import de.rfc1437.ironstorage.core.PasswordRepository
|
||||
import de.rfc1437.ironstorage.core.ShellIcon
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun PasswordScreen(repository: PasswordRepository, modifier: Modifier = Modifier) {
|
||||
val state by repository.state.collectAsStateWithLifecycle()
|
||||
val navigator = rememberListDetailPaneScaffoldNavigator<String>()
|
||||
var actions by remember { mutableStateOf(false) }
|
||||
var passphrase by remember { mutableStateOf("") }
|
||||
val entry = state.selected?.takeIf { it.kind == PasswordKind.Entry }
|
||||
LaunchedEffect(entry?.id) {
|
||||
passphrase = ""
|
||||
navigator.navigateTo(
|
||||
if (entry ==
|
||||
null
|
||||
) {
|
||||
ListDetailPaneScaffoldRole.List
|
||||
} else {
|
||||
ListDetailPaneScaffoldRole.Detail
|
||||
},
|
||||
entry?.id
|
||||
)
|
||||
}
|
||||
LifecycleEventEffect(Lifecycle.Event.ON_STOP) {
|
||||
passphrase = ""
|
||||
repository.lock()
|
||||
}
|
||||
LifecycleEventEffect(Lifecycle.Event.ON_RESUME) {
|
||||
if (state.revision.isNotEmpty()) repository.refresh()
|
||||
}
|
||||
DisposableEffect(repository) { onDispose { repository.lock() } }
|
||||
BackHandler(state.selected != null || state.parent != null) {
|
||||
actions = false
|
||||
repository.back()
|
||||
}
|
||||
val actionLabel = stringResource(R.string.password_actions)
|
||||
Column(modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
TextButton(
|
||||
enabled = !state.loading && (state.parent != null || state.selected != null),
|
||||
onClick = repository::back
|
||||
) {
|
||||
Text(stringResource(R.string.password_back))
|
||||
}
|
||||
TextButton(enabled = !state.loading, onClick = repository::refresh) {
|
||||
Text(stringResource(R.string.password_refresh))
|
||||
}
|
||||
}
|
||||
ListDetailPaneScaffold(
|
||||
directive = navigator.scaffoldDirective,
|
||||
value = navigator.scaffoldValue,
|
||||
modifier = Modifier.weight(1f),
|
||||
listPane = {
|
||||
AnimatedPane {
|
||||
LazyColumn(Modifier.fillMaxSize()) {
|
||||
item {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(
|
||||
state.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.semantics { heading() }
|
||||
)
|
||||
Text(state.path.ifEmpty { stringResource(R.string.password_root) })
|
||||
if (state.loading) CircularProgressIndicator()
|
||||
state.error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
if (state.stale) Text(stringResource(R.string.password_changed))
|
||||
}
|
||||
}
|
||||
if (!state.loading && state.error == null && state.rows.isEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
stringResource(R.string.password_empty),
|
||||
Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
items(state.rows, key = { it.id }) { row ->
|
||||
val rowActions =
|
||||
stringResource(R.string.password_actions_for, row.title)
|
||||
val kindLabel = stringResource(
|
||||
if (row.kind ==
|
||||
PasswordKind.Folder
|
||||
) {
|
||||
R.string.password_folder
|
||||
} else {
|
||||
R.string.password_entry
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = { Text(row.title) },
|
||||
supportingContent = { Text(row.detail) },
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painterResource(
|
||||
when (row.icon) {
|
||||
ShellIcon.Folder -> R.drawable.ic_folder
|
||||
ShellIcon.Key -> R.drawable.ic_key
|
||||
else -> error("Invalid Rust navigation icon")
|
||||
}
|
||||
),
|
||||
null
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
TextButton(
|
||||
modifier = Modifier.semantics {
|
||||
contentDescription =
|
||||
rowActions
|
||||
},
|
||||
enabled = !state.loading,
|
||||
onClick = {
|
||||
actions = true
|
||||
repository.inspect(row.id, state.revision)
|
||||
}
|
||||
) { Text(actionLabel) }
|
||||
},
|
||||
modifier = Modifier.combinedClickable(
|
||||
enabled = !state.loading,
|
||||
onClickLabel = stringResource(R.string.password_open),
|
||||
onLongClickLabel = rowActions,
|
||||
onClick = {
|
||||
actions = false
|
||||
repository.activate(row.id, state.revision)
|
||||
},
|
||||
onLongClick = {
|
||||
actions = true
|
||||
repository.inspect(row.id, state.revision)
|
||||
}
|
||||
).semantics {
|
||||
stateDescription = kindLabel
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction(rowActions) {
|
||||
actions = true
|
||||
repository.inspect(row.id, state.revision)
|
||||
true
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
detailPane = {
|
||||
AnimatedPane {
|
||||
LazyColumn(
|
||||
Modifier.fillMaxSize().padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
if (entry == null) {
|
||||
item { Text(stringResource(R.string.password_choose)) }
|
||||
} else {
|
||||
item {
|
||||
Text(
|
||||
entry.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
}
|
||||
)
|
||||
Text(entry.path)
|
||||
if (state.loading) CircularProgressIndicator()
|
||||
state.error?.let {
|
||||
Text(it, color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
val sections = state.sections
|
||||
if (sections == null) {
|
||||
item {
|
||||
Text(entry.detail)
|
||||
OutlinedTextField(
|
||||
passphrase,
|
||||
{
|
||||
passphrase = it
|
||||
},
|
||||
label = {
|
||||
Text(stringResource(R.string.setup_passphrase))
|
||||
},
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
singleLine = true,
|
||||
enabled = !state.loading
|
||||
)
|
||||
Button(enabled = !state.loading, onClick = {
|
||||
val submitted = passphrase
|
||||
passphrase =
|
||||
""
|
||||
repository.unlock(submitted)
|
||||
}) { Text(stringResource(R.string.password_unlock)) }
|
||||
}
|
||||
} else {
|
||||
item {
|
||||
TextButton(onClick = repository::lock) {
|
||||
Text(stringResource(R.string.password_lock))
|
||||
}
|
||||
}
|
||||
sections.forEach { section ->
|
||||
item {
|
||||
Text(
|
||||
section.title,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.semantics {
|
||||
heading()
|
||||
}
|
||||
)
|
||||
}
|
||||
items(section.fields, key = { it.id.toString() }) { field ->
|
||||
ListItem(headlineContent = {
|
||||
Text(field.label)
|
||||
}, supportingContent = {
|
||||
Column {
|
||||
Text(field.value)
|
||||
field.diagnostic?.let { Text(it) }
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
if (actions && state.selected != null && !state.loading) {
|
||||
val selected = requireNotNull(state.selected)
|
||||
AlertDialog(
|
||||
onDismissRequest = { actions = false },
|
||||
title = {
|
||||
Text(selected.title)
|
||||
},
|
||||
text = { Text(selected.detail + "\n" + selected.path) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
actions = false
|
||||
repository.activate(selected.id, state.revision)
|
||||
}) { Text(stringResource(R.string.password_open)) }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
actions = false
|
||||
}) { Text(stringResource(R.string.password_close)) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
@@ -52,7 +53,15 @@ fun ShellScreen(viewModel: ShellViewModel, setupForm: SetupFormViewModel) {
|
||||
modifier = Modifier.fillMaxSize().padding(padding).padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.app_name), style = MaterialTheme.typography.headlineLarge)
|
||||
val browsing = !snapshot.fixture && snapshot.nativeFailure == null &&
|
||||
snapshot.storageRecovery == null && snapshot.state != ShellState.Empty &&
|
||||
snapshot.selectedTab == ShellTab.Passwords
|
||||
if (!browsing) {
|
||||
Text(
|
||||
stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.headlineLarge
|
||||
)
|
||||
}
|
||||
val page = snapshot.pages.firstOrNull { it.tab == snapshot.selectedTab }
|
||||
val nativeFailure = snapshot.nativeFailure
|
||||
val title = if (nativeFailure != null) {
|
||||
@@ -80,6 +89,9 @@ fun ShellScreen(viewModel: ShellViewModel, setupForm: SetupFormViewModel) {
|
||||
(snapshot.state == ShellState.Empty || snapshot.selectedTab == ShellTab.Preferences)
|
||||
) {
|
||||
OnboardingScreen(viewModel.onboarding, setupForm)
|
||||
} else if (browsing) {
|
||||
LaunchedEffect(viewModel) { viewModel.openPasswords() }
|
||||
PasswordScreen(viewModel.passwords, Modifier.weight(1f))
|
||||
} else {
|
||||
Text(
|
||||
title,
|
||||
|
||||
@@ -1,18 +1,58 @@
|
||||
package de.rfc1437.ironstorage.features
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.rfc1437.ironstorage.core.OnboardingRepository
|
||||
import de.rfc1437.ironstorage.core.PasswordRepository
|
||||
import de.rfc1437.ironstorage.core.ShellRepository
|
||||
import de.rfc1437.ironstorage.core.ShellTab
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@HiltViewModel
|
||||
class ShellViewModel @Inject constructor(
|
||||
private val repository: ShellRepository,
|
||||
val onboarding: OnboardingRepository
|
||||
val onboarding: OnboardingRepository,
|
||||
val passwords: PasswordRepository,
|
||||
private val saved: SavedStateHandle
|
||||
) : ViewModel() {
|
||||
val snapshot = repository.snapshot
|
||||
|
||||
fun selectTab(tab: ShellTab) = repository.selectTab(tab)
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
passwords.state.collect { value ->
|
||||
if (!value.loading && value.error == null && value.revision.isNotEmpty()) {
|
||||
saved["password.path"] = value.path
|
||||
saved["password.selection"] = value.selected?.id
|
||||
saved["password.revision"] = value.revision
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private var passwordsRestored = false
|
||||
fun openPasswords() {
|
||||
if (!passwordsRestored) {
|
||||
passwordsRestored = true
|
||||
passwords.restore(
|
||||
saved["password.path"] ?: "",
|
||||
saved["password.selection"],
|
||||
saved["password.revision"]
|
||||
)
|
||||
} else {
|
||||
passwords.refresh()
|
||||
}
|
||||
}
|
||||
fun selectTab(tab: ShellTab) {
|
||||
if (tab !=
|
||||
ShellTab.Passwords
|
||||
) {
|
||||
passwords.lock()
|
||||
}
|
||||
repository.selectTab(tab)
|
||||
}
|
||||
override fun onCleared() {
|
||||
passwords.lock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||
<path android:fillColor="#FF000000" android:pathData="M10,4H2v16h20V6H12z" />
|
||||
</vector>
|
||||
@@ -30,4 +30,18 @@
|
||||
<string name="setup_replace_token">Replace application token</string>
|
||||
<string name="setup_back">Back to setup choices</string>
|
||||
<string name="setup_retry">Reload setup state</string>
|
||||
<string name="password_back">Back</string>
|
||||
<string name="password_refresh">Refresh</string>
|
||||
<string name="password_root">Password Store</string>
|
||||
<string name="password_changed">The repository changed. Stale selections were cleared.</string>
|
||||
<string name="password_empty">This folder is empty.</string>
|
||||
<string name="password_choose">Select an entry to view it.</string>
|
||||
<string name="password_actions">Actions</string>
|
||||
<string name="password_actions_for">Actions for %1$s</string>
|
||||
<string name="password_folder">Folder</string>
|
||||
<string name="password_entry">Locked password entry</string>
|
||||
<string name="password_open">Open</string>
|
||||
<string name="password_close">Close</string>
|
||||
<string name="password_unlock">Unlock</string>
|
||||
<string name="password_lock">Lock</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user