This commit is contained in:
23 files changed
+2224
-53
No files matched your search
@@ -13,6 +13,7 @@ import androidx.compose.material.icons.automirrored.filled.ListAlt
|
||||
import androidx.compose.material.icons.filled.ChatBubbleOutline
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Language
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Palette
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material3.Card
|
||||
@@ -41,6 +42,7 @@ import com.m57.hermescontrol.SettingsBehavior
|
||||
import com.m57.hermescontrol.SettingsChat
|
||||
import com.m57.hermescontrol.SettingsConnection
|
||||
import com.m57.hermescontrol.SettingsLanguage
|
||||
import com.m57.hermescontrol.SettingsVault
|
||||
import com.m57.hermescontrol.theme.ThemePreference
|
||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
@@ -118,6 +120,12 @@ fun SettingsScreen(
|
||||
},
|
||||
onClick = { NavigationController.navigateTo(SettingsBehavior) },
|
||||
),
|
||||
SettingsRow(
|
||||
icon = Icons.Filled.Lock,
|
||||
label = stringResource(R.string.settings_sec_vault),
|
||||
summary = stringResource(R.string.settings_summary_vault),
|
||||
onClick = { NavigationController.navigateTo(SettingsVault) },
|
||||
),
|
||||
SettingsRow(
|
||||
icon = Icons.Filled.Info,
|
||||
label = stringResource(R.string.settings_sec_about),
|
||||
|
||||
@@ -15,8 +15,11 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -39,6 +42,9 @@ import com.m57.hermescontrol.ui.settings.components.ConnectionSection
|
||||
import com.m57.hermescontrol.ui.settings.components.LanguageSection
|
||||
import com.m57.hermescontrol.ui.settings.components.TestConnectionButton
|
||||
import com.m57.hermescontrol.ui.settings.components.TestResultCard
|
||||
import com.m57.hermescontrol.ui.settings.components.VaultItemsSection
|
||||
import com.m57.hermescontrol.ui.settings.components.VaultSourcesSection
|
||||
import com.m57.hermescontrol.ui.settings.components.VaultUnlockDialog
|
||||
|
||||
/**
|
||||
* Drill-down sub-pages for Settings. Each is its own NavKey destination
|
||||
@@ -288,3 +294,54 @@ internal fun SettingsAboutPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SettingsVaultPage(
|
||||
onBack: () -> Unit,
|
||||
viewModel: SettingsVaultViewModel = viewModel { SettingsVaultViewModel() },
|
||||
) {
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(state.toastMessage) {
|
||||
state.toastMessage?.let { message ->
|
||||
snackbarHostState.showSnackbar(message)
|
||||
viewModel.clearToast()
|
||||
}
|
||||
}
|
||||
|
||||
HermesScaffold(
|
||||
title = { Text(stringResource(R.string.settings_sec_vault)) },
|
||||
navigationIcon = NavIcon.Back(onBack),
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||
drawerGesturesEnabled = false,
|
||||
) {
|
||||
Column(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
VaultSourcesSection(
|
||||
sources = state.sources,
|
||||
isActionInProgress = state.isActionInProgress,
|
||||
onToggleSource = viewModel::toggleSource,
|
||||
onUnlockClick = viewModel::showUnlockDialog,
|
||||
onLockClick = { source -> viewModel.lockSource(source.name) },
|
||||
onLockAllClick = { viewModel.lockSource(null) },
|
||||
)
|
||||
|
||||
VaultItemsSection(items = state.items)
|
||||
}
|
||||
|
||||
state.unlockDialogSource?.let { source ->
|
||||
VaultUnlockDialog(
|
||||
source = source,
|
||||
onConfirm = { password -> viewModel.unlockSource(source.name, password) },
|
||||
onDismiss = viewModel::dismissUnlockDialog,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.m57.hermescontrol.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.data.model.VaultItem
|
||||
import com.m57.hermescontrol.data.model.VaultSource
|
||||
import com.m57.hermescontrol.data.ws.VaultRepository
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class SettingsVaultUiState(
|
||||
val sources: List<VaultSource> = emptyList(),
|
||||
val items: List<VaultItem> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val isActionInProgress: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val toastMessage: String? = null,
|
||||
val unlockDialogSource: VaultSource? = null,
|
||||
)
|
||||
|
||||
class SettingsVaultViewModel(
|
||||
private val vaultRepo: VaultRepository = VaultRepository,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(SettingsVaultUiState())
|
||||
val uiState: StateFlow<SettingsVaultUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadVaultData()
|
||||
}
|
||||
|
||||
fun loadVaultData() {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||
try {
|
||||
val sources = vaultRepo.getSources()
|
||||
val items =
|
||||
try {
|
||||
vaultRepo.listItems()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
sources = sources,
|
||||
items = items,
|
||||
isLoading = false,
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoading = false,
|
||||
errorMessage = e.message ?: "Failed to load vault status",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSource(
|
||||
source: VaultSource,
|
||||
enabled: Boolean,
|
||||
) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
_uiState.update { it.copy(isActionInProgress = true) }
|
||||
try {
|
||||
vaultRepo.setSourceEnabled(source.name, enabled)
|
||||
val sources = vaultRepo.getSources()
|
||||
_uiState.update { it.copy(sources = sources, isActionInProgress = false) }
|
||||
} catch (e: Exception) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isActionInProgress = false,
|
||||
toastMessage = "Failed to update ${source.displayName}: ${e.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unlockSource(
|
||||
name: String,
|
||||
password: String,
|
||||
) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
_uiState.update { it.copy(isActionInProgress = true, unlockDialogSource = null) }
|
||||
try {
|
||||
val success = vaultRepo.unlockSource(name, password)
|
||||
val sources = vaultRepo.getSources()
|
||||
val items =
|
||||
try {
|
||||
vaultRepo.listItems()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
sources = sources,
|
||||
items = items,
|
||||
isActionInProgress = false,
|
||||
toastMessage = if (success) "Unlocked $name" else "Failed to unlock $name",
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isActionInProgress = false,
|
||||
toastMessage = "Error unlocking $name: ${e.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun lockSource(name: String? = null) {
|
||||
viewModelScope.launch(ioDispatcher) {
|
||||
_uiState.update { it.copy(isActionInProgress = true) }
|
||||
try {
|
||||
vaultRepo.lockSource(name)
|
||||
val sources = vaultRepo.getSources()
|
||||
val items =
|
||||
try {
|
||||
vaultRepo.listItems()
|
||||
} catch (_: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
sources = sources,
|
||||
items = items,
|
||||
isActionInProgress = false,
|
||||
toastMessage = if (name != null) "Locked $name" else "All password managers locked",
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isActionInProgress = false,
|
||||
toastMessage = "Error locking vault: ${e.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun showUnlockDialog(source: VaultSource) {
|
||||
_uiState.update { it.copy(unlockDialogSource = source) }
|
||||
}
|
||||
|
||||
fun dismissUnlockDialog() {
|
||||
_uiState.update { it.copy(unlockDialogSource = null) }
|
||||
}
|
||||
|
||||
fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.m57.hermescontrol.ui.settings.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.LockOpen
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.Key
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.VaultItem
|
||||
import com.m57.hermescontrol.data.model.VaultSource
|
||||
import com.m57.hermescontrol.theme.LocalHermesStatusColors
|
||||
|
||||
@Composable
|
||||
fun VaultSourcesSection(
|
||||
sources: List<VaultSource>,
|
||||
isActionInProgress: Boolean,
|
||||
onToggleSource: (VaultSource, Boolean) -> Unit,
|
||||
onUnlockClick: (VaultSource) -> Unit,
|
||||
onLockClick: (VaultSource) -> Unit,
|
||||
onLockAllClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_sources_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.vault_sources_desc),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
sources.forEachIndexed { index, source ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
VaultSourceRow(
|
||||
source = source,
|
||||
isActionInProgress = isActionInProgress,
|
||||
onToggle = { onToggleSource(source, it) },
|
||||
onUnlock = { onUnlockClick(source) },
|
||||
onLock = { onLockClick(source) },
|
||||
)
|
||||
}
|
||||
|
||||
val hasUnlockedExternal = sources.any { it.needsUnlock && it.unlocked }
|
||||
if (hasUnlockedExternal) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
OutlinedButton(
|
||||
onClick = onLockAllClick,
|
||||
modifier = Modifier.fillMaxWidth().testTag("vault_lock_all_button"),
|
||||
enabled = !isActionInProgress,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.vault_lock_all))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VaultSourceRow(
|
||||
source: VaultSource,
|
||||
isActionInProgress: Boolean,
|
||||
onToggle: (Boolean) -> Unit,
|
||||
onUnlock: () -> Unit,
|
||||
onLock: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = source.displayName,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
if (source.name == "local") {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = LocalHermesStatusColors.current.success.copy(alpha = 0.15f),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_source_unlocked),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = LocalHermesStatusColors.current.success,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
} else if (!source.installed) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_source_not_installed),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
} else if (source.unlocked) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = LocalHermesStatusColors.current.success.copy(alpha = 0.15f),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_source_unlocked),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = LocalHermesStatusColors.current.success,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = MaterialTheme.colorScheme.errorContainer,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_source_locked),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source.name != "local" && source.installed) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
if (source.unlocked) {
|
||||
FilledTonalButton(
|
||||
onClick = onLock,
|
||||
enabled = !isActionInProgress,
|
||||
modifier = Modifier.height(32.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Lock,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.vault_source_lock),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Button(
|
||||
onClick = onUnlock,
|
||||
enabled = !isActionInProgress && source.enabled,
|
||||
modifier = Modifier.height(32.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.LockOpen,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.vault_source_unlock),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (source.name != "local" && source.installed) {
|
||||
Switch(
|
||||
checked = source.enabled,
|
||||
onCheckedChange = onToggle,
|
||||
enabled = !isActionInProgress,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VaultItemsSection(
|
||||
items: List<VaultItem>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Card(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||
),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "${stringResource(R.string.vault_items_title)} (${items.size})",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
|
||||
if (items.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_items_empty),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
)
|
||||
} else {
|
||||
items.forEachIndexed { index, item ->
|
||||
if (index > 0) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||
)
|
||||
}
|
||||
VaultItemRow(item = item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VaultItemRow(item: VaultItem) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Key,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = item.label.ifBlank { item.origin ?: item.id },
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
)
|
||||
val subtext = item.identifier ?: item.origin
|
||||
if (!subtext.isNullOrBlank()) {
|
||||
Text(
|
||||
text = subtext,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
) {
|
||||
Text(
|
||||
text = item.backend,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VaultUnlockDialog(
|
||||
source: VaultSource,
|
||||
onConfirm: (String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var password by remember { mutableStateOf("") }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(stringResource(R.string.vault_unlock_dialog_title, source.displayName)) },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
text = stringResource(R.string.vault_unlock_desc, source.displayName),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text(stringResource(R.string.vault_unlock_password_label)) },
|
||||
modifier = Modifier.fillMaxWidth().testTag("dialog_vault_password_input"),
|
||||
singleLine = true,
|
||||
visualTransformation =
|
||||
if (passwordVisible) {
|
||||
VisualTransformation.None
|
||||
} else {
|
||||
PasswordVisualTransformation()
|
||||
},
|
||||
keyboardOptions =
|
||||
KeyboardOptions(
|
||||
autoCorrectEnabled = false,
|
||||
keyboardType = KeyboardType.Password,
|
||||
),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
imageVector =
|
||||
if (passwordVisible) {
|
||||
Icons.Filled.VisibilityOff
|
||||
} else {
|
||||
Icons.Filled.Visibility
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
if (password.isNotBlank()) {
|
||||
onConfirm(password)
|
||||
}
|
||||
},
|
||||
enabled = password.isNotBlank(),
|
||||
) {
|
||||
Text(stringResource(R.string.vault_unlock_dialog_confirm))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.chat_dismiss))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user