Merge pull request #1192 from yogeshpaliyal/material3Expressive

Improve UI and UX for App
This commit is contained in:
Yogesh Choudhary Paliyal
2025-05-18 16:08:33 +05:30
committed by GitHub
28 changed files with 325 additions and 290 deletions
+3 -1
View File
@@ -130,7 +130,9 @@ dependencies {
implementation(Deps.Lifecycle.runtimeCompose)
implementation("androidx.navigation:navigation-compose:2.9.0")
implementation("androidx.compose.material3:material3:1.2.1")
implementation("androidx.compose.material3:material3:1.4.0-alpha14")
implementation("androidx.compose.material3:material3-android:1.4.0-alpha14")
implementation("com.google.accompanist:accompanist-themeadapter-material3:0.36.0")
implementation("androidx.appcompat:appcompat:1.7.0")
@@ -17,11 +17,11 @@ class MyApplication : CommonMyApplication() {
private var timeToLaunchActivity : Long? = null
fun activityLaunchTriggered() {
fun knownActivityLaunchTriggered() {
timeToLaunchActivity = SystemClock.uptimeMillis()
}
fun isActivityLaunchTriggered() : Boolean {
fun isKnownActivityLaunchTriggered() : Boolean {
val mTimeToLaunchActivity = timeToLaunchActivity ?: return false
timeToLaunchActivity = null
return SystemClock.uptimeMillis() - mTimeToLaunchActivity < 1000
@@ -30,11 +30,13 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -47,6 +49,7 @@ import com.yogeshpaliyal.common.utils.openLink
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultTopAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.PreferenceItem
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
@@ -54,15 +57,12 @@ import org.reduxkotlin.compose.rememberTypedDispatcher
@Composable
fun AboutScreen() {
val dispatchAction = rememberTypedDispatcher<Action>()
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
bottomBar = {
// Add back button to bottom bar
DefaultBottomAppBar(
showBackButton = true
)
}
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
DefaultTopAppBar(title = R.string.app_name, scrollBehavior = scrollBehavior)
},
) { contentPadding ->
Surface(
modifier = Modifier
@@ -98,9 +98,11 @@ private fun MainContent() {
horizontalAlignment = Alignment.CenterHorizontally
) {
// App Info Card
ElevatedCard(
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
shape = RoundedCornerShape(16.dp)
) {
Column(
@@ -124,13 +126,13 @@ private fun MainContent() {
Spacer(modifier = Modifier.height(16.dp))
// App Name
Text(
text = stringResource(id = R.string.app_name),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
// // App Name
// Text(
// text = stringResource(id = R.string.app_name),
// style = MaterialTheme.typography.headlineMedium,
// fontWeight = FontWeight.Bold,
// color = MaterialTheme.colorScheme.primary
// )
Spacer(modifier = Modifier.height(4.dp))
@@ -203,9 +205,11 @@ private fun MainContent() {
textAlign = TextAlign.Start
)
ElevatedCard(
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
shape = RoundedCornerShape(16.dp)
) {
Column(modifier = Modifier.fillMaxWidth()) {
@@ -40,6 +40,7 @@ import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.backup.components.BackSettingOptions
import com.yogeshpaliyal.keypass.ui.backup.components.BackupDialogs
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultTopAppBar
import com.yogeshpaliyal.keypass.ui.nav.LocalUserSettings
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
@@ -131,10 +132,8 @@ fun BackupScreen(state: BackupScreenState) {
)
})
Scaffold(bottomBar = {
DefaultBottomAppBar(
showBackButton = true,
)
Scaffold(topBar = {
DefaultTopAppBar(showBackButton = true, title = R.string.credentials_backups, subtitle = R.string.backup_screen_desc)
}) { contentPadding ->
Surface(modifier = Modifier.padding(contentPadding).fillMaxSize()) {
Column(
@@ -143,25 +142,6 @@ fun BackupScreen(state: BackupScreenState) {
.verticalScroll(rememberScrollState())
.padding(16.dp),
) {
// Header Section
Text(
text = stringResource(id = R.string.credentials_backups),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.backup_screen_desc), // New string for description
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
// Backup Settings Options
BackSettingOptions(state, updatedState = {
@@ -15,7 +15,7 @@ class KeyPassBackupDirectoryPick : ActivityResultContracts.OpenDocument() {
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
(context.applicationContext as? MyApplication)?.activityLaunchTriggered()
(context.applicationContext as? MyApplication)?.knownActivityLaunchTriggered()
return intent
}
@@ -2,7 +2,6 @@ package com.yogeshpaliyal.keypass.ui.backupsImport
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -39,7 +38,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
@@ -52,6 +50,7 @@ import com.yogeshpaliyal.keypass.importer.ChromeAccountImporter
import com.yogeshpaliyal.keypass.importer.KeePassAccountImporter
import com.yogeshpaliyal.keypass.importer.KeyPassAccountImporter
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultTopAppBar
import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
@@ -129,20 +128,24 @@ fun BackupImporter(state: BackupImporterState, mViewModel: DashboardViewModel =
})
result?.let {
state.selectedImported?.readFileGetAction(result)?.let { it1 ->
state.selectedImported?.readFileGetAction(result)?.let { it1 ->
isLoading = false
dispatchAction(it1)
dispatchAction(it1)
}
}
Scaffold(
bottomBar = {
DefaultBottomAppBar(
showBackButton = true
topBar = {
DefaultTopAppBar(
showBackButton = true,
title = R.string.restore_credentials,
subtitle = R.string.restore_credentials_desc
)
}
) { paddingValues ->
Box(modifier = Modifier.padding(paddingValues).fillMaxSize()) {
Box(modifier = Modifier
.padding(paddingValues)
.fillMaxSize()) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
@@ -157,25 +160,7 @@ fun BackupImporter(state: BackupImporterState, mViewModel: DashboardViewModel =
Column(
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = stringResource(id = R.string.restore_credentials),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(id = R.string.restore_credentials_desc),
style = MaterialTheme.typography.bodyLarge,
textAlign = TextAlign.Start,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
// Info card
Card(
modifier = Modifier.fillMaxWidth(),
@@ -190,11 +175,11 @@ fun BackupImporter(state: BackupImporterState, mViewModel: DashboardViewModel =
color = MaterialTheme.colorScheme.onSecondaryContainer
)
}
Spacer(modifier = Modifier.height(16.dp))
}
}
items(importOptions) { option ->
ImportOptionCard(
option = option,
@@ -212,11 +197,13 @@ fun BackupImporter(state: BackupImporterState, mViewModel: DashboardViewModel =
@Composable
private fun ImportOptionCard(option: ImportOption, onClick: () -> Unit) {
val desc = option.importer.getImporterDesc()
ElevatedCard(
Card(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
),
onClick = onClick,
shape = RoundedCornerShape(12.dp)
) {
@@ -241,7 +228,7 @@ private fun ImportOptionCard(option: ImportOption, onClick: () -> Unit) {
modifier = Modifier.size(28.dp)
)
}
Column(
modifier = Modifier
.padding(start = 16.dp)
@@ -252,14 +239,14 @@ private fun ImportOptionCard(option: ImportOption, onClick: () -> Unit) {
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
Text(
text = if (desc == null) "" else stringResource(id = desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.Filled.FileUpload,
contentDescription = "Import",
@@ -1,10 +1,6 @@
package com.yogeshpaliyal.keypass.ui.changeDefaultPasswordLength
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -12,25 +8,17 @@ import androidx.compose.foundation.layout.fillMaxSize
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.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material.icons.rounded.Done
import androidx.compose.material.icons.rounded.Info
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
@@ -50,6 +38,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultTopAppBar
import com.yogeshpaliyal.keypass.ui.generate.ui.components.PasswordLengthInput
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
@@ -66,7 +55,13 @@ fun ChangeDefaultPasswordLengthScreen(
val state by viewModel.viewState.collectAsState()
Scaffold(
bottomBar = { BottomBar(dispatchAction, viewModel) }
topBar = {
DefaultTopAppBar(
title = R.string.choose_default_password_length,
subtitle = R.string.choose_default_password_length_desc
)
},
floatingActionButton = { FloatingActionButton(dispatchAction, viewModel) }
) { contentPadding ->
Surface(
modifier = Modifier
@@ -97,16 +92,11 @@ private fun ChangeDefaultPasswordLengthContent(
) {
// Password length input with improved UI
Text(
text = stringResource(R.string.choose_default_password_length),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold,
textAlign = TextAlign.Start
)
ElevatedCard(
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 1.dp)
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(
modifier = Modifier.padding(16.dp),
@@ -130,38 +120,6 @@ private fun ChangeDefaultPasswordLengthContent(
}
Spacer(modifier = Modifier.height(8.dp))
// Information card
InfoCard()
}
}
@Composable
private fun InfoCard() {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.7f)
)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(24.dp)
)
Text(
text = "This length will be used as default when generating new passwords. " +
"Longer passwords provide better security.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.padding(start = 12.dp)
)
}
}
}
@@ -231,44 +189,29 @@ private fun PasswordStrengthIndicator(strength: PasswordStrength) {
}
@Composable
private fun BottomBar(
private fun FloatingActionButton(
dispatchAction: TypedDispatcher<Action>,
viewModel: ChangeDefaultPasswordLengthViewModel
) {
val context = LocalContext.current
BottomAppBar(
actions = {
// Add back button on the left side
IconButton(
onClick = { dispatchAction(GoBackAction) }
) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
contentDescription = "Cancel",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
FloatingActionButton(
modifier = Modifier.testTag("save"),
onClick = {
// Save new password length
viewModel.updatePasswordLength(context) {
// Close screen
dispatchAction(GoBackAction)
}
},
floatingActionButton = {
FloatingActionButton(
modifier = Modifier.testTag("save"),
onClick = {
// Save new password length
viewModel.updatePasswordLength(context) {
// Close screen
dispatchAction(GoBackAction)
}
},
containerColor = MaterialTheme.colorScheme.primaryContainer
) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Done),
contentDescription = "Save Changes",
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
)
containerColor = MaterialTheme.colorScheme.primaryContainer
) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Done),
contentDescription = "Save Changes",
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
@Preview(name = "ChangeDefaultPasswordLength", showBackground = true, showSystemUi = true)
@@ -33,6 +33,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
@@ -42,6 +43,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -54,6 +56,7 @@ import com.yogeshpaliyal.common.utils.setKeyPassPassword
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.auth.components.PasswordInputField
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultTopAppBar
import com.yogeshpaliyal.keypass.ui.nav.LocalUserSettings
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
@@ -98,11 +101,15 @@ fun ChangePassword(state: ChangeAppPasswordState) {
}
var showInfoCard by remember { mutableStateOf(false) }
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
Scaffold(
bottomBar = {
DefaultBottomAppBar(
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
DefaultTopAppBar(
showBackButton = true,
title = R.string.change_app_password,
scrollBehavior = scrollBehavior
)
}
) { contentPadding ->
@@ -115,14 +122,6 @@ fun ChangePassword(state: ChangeAppPasswordState) {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Header section
Text(
text = stringResource(id = R.string.change_app_password),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
Image(
painter = painterResource(R.drawable.ic_undraw_unlock_24mb),
contentDescription = "Change Password Illustration",
@@ -150,9 +149,8 @@ fun ChangePassword(state: ChangeAppPasswordState) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
),
shape = RoundedCornerShape(12.dp)
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(
modifier = Modifier.padding(16.dp)
@@ -177,10 +175,11 @@ fun ChangePassword(state: ChangeAppPasswordState) {
}
// Password input fields in a card
ElevatedCard(
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp),
shape = RoundedCornerShape(16.dp)
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(
modifier = Modifier
@@ -0,0 +1,61 @@
package com.yogeshpaliyal.keypass.ui.commonComponents
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.stringResource
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
import org.reduxkotlin.compose.rememberTypedDispatcher
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun DefaultTopAppBar(
modifier: Modifier = Modifier,
showBackButton: Boolean = true,
extraAction: (@Composable RowScope.() -> Unit)? = null,
title: Int,
subtitle: Int? = null,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
val dispatchAction = rememberTypedDispatcher<Action>()
LargeFlexibleTopAppBar(modifier = modifier, scrollBehavior = scrollBehavior, subtitle = {
if (subtitle != null) {
Text(
text = stringResource(id = subtitle),
)
}
}, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Unspecified, scrolledContainerColor = MaterialTheme.colorScheme.surface),
title = {
Text(
text = stringResource(id = title),
)
}, actions = {
extraAction?.invoke(this)
}, navigationIcon = {
if (showBackButton) {
IconButton(onClick = {
dispatchAction(GoBackAction)
}) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
contentDescription = "Go Back",
tint = MaterialTheme.colorScheme.onSurface
)
}
}
})
}
@@ -33,6 +33,7 @@ import com.yogeshpaliyal.common.constants.ScannerType
import com.yogeshpaliyal.common.utils.TOTPHelper
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.detail.components.BottomBar
import com.yogeshpaliyal.keypass.ui.detail.components.FABAddAccount
import com.yogeshpaliyal.keypass.ui.detail.components.Fields
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
@@ -104,7 +105,7 @@ fun AccountDetailPage(
}
}
Scaffold(
bottomBar = {
topBar = {
BottomBar(
accountModel,
backPressed = goBack,
@@ -120,7 +121,10 @@ fun AccountDetailPage(
openPasswordConfiguration = {
dispatchAction(NavigationAction(PasswordGeneratorState()))
}
) {
)
},
floatingActionButton = {
FABAddAccount{
viewModel.insertOrUpdate(accountModel, goBack)
}
}
@@ -20,7 +20,7 @@ class QRScanner : ActivityResultContract<Int, QRScannerResult>() {
} else {
null
}
(context.applicationContext as? MyApplication)?.activityLaunchTriggered()
(context.applicationContext as? MyApplication)?.knownActivityLaunchTriggered()
return intentIntegration?.setPrompt("")?.createScanIntent() ?: Intent()
}
@@ -7,18 +7,24 @@ import androidx.compose.material.icons.rounded.ArrowBackIosNew
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.Done
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.keypass.R
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun BottomBar(
accountModel: AccountModel,
@@ -26,12 +32,17 @@ fun BottomBar(
onDeleteAccount: () -> Unit,
generateQrCodeClicked: () -> Unit,
openPasswordConfiguration: () -> Unit,
onSaveClicked: () -> Unit
) {
val openDialog = remember { mutableStateOf(false) }
BottomAppBar(
actions = {
LargeFlexibleTopAppBar(
title = {
Text(
text = stringResource(if (accountModel.id == null) R.string.create_account else R.string.edit_account),
color = MaterialTheme.colorScheme.onSurface
)
},
navigationIcon = {
IconButton(onClick = backPressed) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
@@ -39,6 +50,8 @@ fun BottomBar(
tint = MaterialTheme.colorScheme.onSurface
)
}
},
actions = {
IconButton(
modifier = Modifier.testTag("action_configure_password"),
@@ -73,14 +86,6 @@ fun BottomBar(
)
}
}
},
floatingActionButton = {
FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Done),
contentDescription = "Save Changes"
)
}
}
)
@@ -92,3 +97,13 @@ fun BottomBar(
onDeleteAccount
)
}
@Composable
fun FABAddAccount(onSaveClicked: () -> Unit) {
FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Done),
contentDescription = "Save Changes"
)
}
}
@@ -71,7 +71,7 @@ fun AccountsList(accounts: List<AccountModel>? = null) {
) {
items(it) { account ->
Account(
modifier = Modifier.animateItemPlacement(),
modifier = Modifier,
account,
onClick = {
dispatch(NavigationAction(AccountDetailState(it.id)))
@@ -41,7 +41,7 @@ class BottomNavViewModel @Inject constructor(
private fun postListUpdate() {
val newList = if (tagsList.isNullOrEmpty().not()) {
NavigationModel.navigationMenuItems + NavigationModelItem.NavDivider("Tags") + (
mutableListOf<NavigationModelItem>() + NavigationModelItem.NavDivider("Tags", true) + (
tagsList?.filter { it != null }
?.map { NavigationModelItem.NavTagItem(it) } ?: listOf()
)
@@ -112,7 +112,7 @@ class DashboardComposeActivity : AppCompatActivity() {
}
@Composable
fun Dashboard() {
fun Dashboard(viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel()) {
val systemBackPress by selectState<KeyPassState, Boolean> { this.systemBackPress }
val context = LocalContext.current
@@ -124,7 +124,7 @@ fun Dashboard() {
// Call this like any other SideEffect in your composable
LifecycleEventEffect(Lifecycle.Event.ON_PAUSE) {
if (userSettings.autoLockEnabled == true &&
(context.applicationContext as? MyApplication)?.isActivityLaunchTriggered() == false) {
(context.applicationContext as? MyApplication)?.isKnownActivityLaunchTriggered() == false) {
dispatch(NavigationAction(AuthState.Login))
}
}
@@ -142,12 +142,12 @@ fun Dashboard() {
onDispose { dispatch(UpdateContextAction(null)) }
}
Scaffold(bottomBar = { KeyPassBottomBar() }, modifier = Modifier.safeDrawingPadding()) {
Scaffold(bottomBar = { KeyPassBottomBar(viewModel) }, modifier = Modifier.safeDrawingPadding()) {
paddingValues ->
Surface(modifier = Modifier.padding(paddingValues)) {
CurrentPage()
DashboardBottomSheet()
DashboardBottomSheet(viewModel)
}
}
}
@@ -1,6 +1,7 @@
package com.yogeshpaliyal.keypass.ui.nav
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.nav.NavigationModelItem.NavMenuItem
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
@@ -11,20 +12,20 @@ object NavigationModel {
const val GENERATE_PASSWORD = 1
const val ADD_TOPT = 2
var navigationMenuItems = mutableListOf(
NavigationModelItem.NavMenuItem(
id = HOME,
icon = R.drawable.ic_twotone_home_24,
titleRes = R.string.home,
checked = false,
action = NavigationAction(HomeState(), true)
),
NavigationModelItem.NavMenuItem(
id = GENERATE_PASSWORD,
icon = R.drawable.ic_twotone_vpn_key_24,
titleRes = R.string.generate_password,
checked = false,
action = IntentNavigation.GeneratePassword
)
var navigationMenuItems = mutableListOf<NavMenuItem>(
// NavigationModelItem.NavMenuItem(
// id = HOME,
// icon = R.drawable.ic_twotone_home_24,
// titleRes = R.string.home,
// checked = false,
// action = NavigationAction(HomeState(), true)
// ),
// NavigationModelItem.NavMenuItem(
// id = GENERATE_PASSWORD,
// icon = R.drawable.ic_twotone_vpn_key_24,
// titleRes = R.string.generate_password,
// checked = false,
// action = IntentNavigation.GeneratePassword
// )
)
}
@@ -18,7 +18,7 @@ sealed class NavigationModelItem {
* A class which is used to show a section divider (a subtitle and underline) between
* sections of different NavigationModelItem types.
*/
data class NavDivider(val title: String) : NavigationModelItem()
data class NavDivider(val title: String, val hideDivider: Boolean = false) : NavigationModelItem()
data class NavTagItem(val tag: String) : NavigationModelItem()
}
@@ -21,19 +21,19 @@ import org.reduxkotlin.compose.rememberDispatcher
import org.reduxkotlin.compose.selectState
@Composable
fun DashboardBottomSheet() {
fun DashboardBottomSheet(viewModel: BottomNavViewModel) {
val bottomSheetState by selectState<KeyPassState, BottomSheetState?> { this.bottomSheet }
if (bottomSheetState?.isBottomSheetOpen != true) {
return
}
OptionBottomBar()
OptionBottomBar(viewModel)
}
@Composable
fun OptionBottomBar(
viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
viewModel: BottomNavViewModel
) {
val dispatchAction = rememberDispatcher()
@@ -1,52 +1,95 @@
package com.yogeshpaliyal.keypass.ui.nav.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Menu
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.VpnKey
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.IconToggleButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
import com.yogeshpaliyal.keypass.ui.nav.BottomNavViewModel
import com.yogeshpaliyal.keypass.ui.redux.actions.BottomSheetAction
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState
import com.yogeshpaliyal.keypass.ui.redux.states.SettingsState
import org.reduxkotlin.compose.rememberDispatcher
import org.reduxkotlin.compose.selectState
@Composable
fun KeyPassBottomBar() {
val showMainBottomAppBar by selectState<KeyPassState, Boolean> { this.currentScreen.showMainBottomAppBar }
fun KeyPassBottomBar(viewModel: BottomNavViewModel) {
val currentScreen: ScreenState by selectState<KeyPassState, ScreenState> { this.currentScreen }
val showMainBottomAppBar = currentScreen.showMainBottomAppBar
val dispatchAction = rememberDispatcher()
val navigationItems by viewModel.navigationList.observeAsState()
if (!showMainBottomAppBar) {
return
}
DefaultBottomAppBar(showBackButton = false, extraAction = {
IconButton(onClick = {
dispatchAction(BottomSheetAction.HomeNavigationMenu(true))
IconToggleButton (colors = IconButtonDefaults.iconToggleButtonColors(
checkedContainerColor = MaterialTheme.colorScheme.primaryContainer,
checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer
), checked = currentScreen is HomeState, onCheckedChange = {
dispatchAction(NavigationAction(HomeState(), true))
}) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Menu),
contentDescription = "Menu",
painter = rememberVectorPainter(image = Icons.Outlined.Home),
contentDescription = "Home",
tint = MaterialTheme.colorScheme.onSurface
)
}
IconButton(onClick = {
dispatchAction(IntentNavigation.GeneratePassword)
}) {
Icon(
painter = rememberVectorPainter(image = Icons.Outlined.VpnKey),
contentDescription = "Generate Password",
tint = MaterialTheme.colorScheme.onSurface
)
}
if (navigationItems?.isNotEmpty() == true) {
IconButton(onClick = {
dispatchAction(BottomSheetAction.HomeNavigationMenu(true))
}) {
Icon(
painter = rememberVectorPainter(image = Icons.Outlined.Menu),
contentDescription = "Menu",
tint = MaterialTheme.colorScheme.onSurface
)
}
}
IconToggleButton(colors = IconButtonDefaults.iconToggleButtonColors(
checkedContainerColor = MaterialTheme.colorScheme.primaryContainer,
checkedContentColor = MaterialTheme.colorScheme.onPrimaryContainer
), checked = currentScreen is SettingsState, onCheckedChange = {
dispatchAction(NavigationAction(SettingsState))
}) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Settings),
painter = rememberVectorPainter(image = Icons.Outlined.Settings),
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onSurface
)
@@ -11,6 +11,7 @@ import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -21,24 +22,22 @@ import com.yogeshpaliyal.keypass.ui.nav.NavigationModelItem
@Composable
fun NavItem(item: NavigationModelItem.NavMenuItem, onClick: () -> Unit) {
Row(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true),
onClick = onClick
TextButton(onClick) {
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(1f)
) {
Icon(
painter = painterResource(id = item.icon),
contentDescription = ""
)
.padding(16.dp)
.fillMaxWidth(1f)
) {
Icon(
painter = painterResource(id = item.icon),
contentDescription = ""
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = stringResource(id = item.titleRes),
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.width(32.dp))
Text(
text = stringResource(id = item.titleRes),
style = MaterialTheme.typography.titleMedium
)
}
}
}
@@ -18,12 +18,13 @@ import java.util.Locale
@Composable
fun NavItemSection(divider: NavigationModelItem.NavDivider) {
Column(modifier = Modifier.padding(16.dp)) {
Divider()
Spacer(modifier = Modifier.height(32.dp))
if (!divider.hideDivider) {
Divider()
Spacer(modifier = Modifier.height(32.dp))
}
Text(
text = divider.title.uppercase(Locale.getDefault()),
style = MaterialTheme.typography.labelMedium,
fontSize = TextUnit(12f, TextUnitType.Sp)
style = MaterialTheme.typography.titleLarge
)
}
}
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -16,22 +17,25 @@ import com.yogeshpaliyal.keypass.ui.nav.NavigationModelItem
@Composable
fun NavMenuFolder(folder: NavigationModelItem.NavTagItem, onClick: () -> Unit) {
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
bounded = true
),
onClick = onClick
)
.padding(16.dp)
.fillMaxWidth(1f)
) {
TextButton(onClick) {
Text(
text = folder.tag,
style = MaterialTheme.typography.titleMedium
)
}
// Box(
// modifier = Modifier
// .clickable(
// interactionSource = remember { MutableInteractionSource() },
// indication = rememberRipple(
// bounded = true
// ),
// onClick = onClick
// )
// .padding(16.dp)
// .fillMaxWidth(1f)
//
// ) {
//
// }
}
@@ -40,6 +40,7 @@ import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.common.utils.setPasswordHint
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultBottomAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.DefaultTopAppBar
import com.yogeshpaliyal.keypass.ui.commonComponents.KeyPassInputField
import com.yogeshpaliyal.keypass.ui.nav.LocalUserSettings
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
@@ -61,10 +62,8 @@ fun PasswordHintScreen() {
var showInfoDialog by remember { mutableStateOf(false) }
Scaffold(
bottomBar = {
DefaultBottomAppBar(
showBackButton = true
)
topBar = {
DefaultTopAppBar(title = R.string.app_password_hint, subtitle = R.string.app_password_hint_desc)
}
) { contentPadding ->
Surface(
@@ -78,26 +77,13 @@ fun PasswordHintScreen() {
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Title and Description
Text(
text = stringResource(id = R.string.app_password_hint),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold
)
Text(
text = "A password hint helps you remember your password if you forget it. " +
"Make sure it's a hint only you would understand.",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
// Input Card
ElevatedCard(
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.elevatedCardElevation(defaultElevation = 2.dp)
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerLow
)
) {
Column(
modifier = Modifier.padding(16.dp),
@@ -2,6 +2,7 @@ package com.yogeshpaliyal.keypass.ui.redux.middlewares
import android.content.Intent
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.MyApplication
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity
import com.yogeshpaliyal.keypass.ui.redux.actions.BatchActions
@@ -29,11 +30,13 @@ val intentNavigationMiddleware = middleware<KeyPassState> { store, next, action
private fun Store<KeyPassState>.handleAction(action: Any, state: KeyPassState) {
when (action) {
is IntentNavigation.GeneratePassword -> {
(state.context?.applicationContext as? MyApplication)?.knownActivityLaunchTriggered()
val intent = Intent(state.context, GeneratePasswordActivity::class.java)
state.context?.startActivity(intent)
}
is IntentNavigation.ShareApp -> {
(state.context?.applicationContext as? MyApplication)?.knownActivityLaunchTriggered()
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(
@@ -11,7 +11,6 @@ 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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@@ -25,7 +24,6 @@ import androidx.compose.material.icons.rounded.Share
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.KeyboardArrowDown
import androidx.compose.material.icons.filled.KeyboardArrowUp
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.OutlinedTextField
@@ -339,7 +337,7 @@ fun MySettingCompose() {
PreferenceType.AUTO_FILL -> {
val autoFillClick = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
{
(context.applicationContext as? MyApplication)?.activityLaunchTriggered()
(context.applicationContext as? MyApplication)?.knownActivityLaunchTriggered()
context.enableAutoFillService()
}
} else null
@@ -18,7 +18,7 @@ class OpenKeyPassBackup<T : AccountsImporter>(val importer: T?) : ActivityResult
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
(context.applicationContext as? MyApplication)?.activityLaunchTriggered()
(context.applicationContext as? MyApplication)?.knownActivityLaunchTriggered()
return intent
}
+4
View File
@@ -50,6 +50,8 @@
<string name="backup_desc">Backups are encrypted with a passphrase and stored on your device</string>
<string name="yes">Yes</string>
<string name="create_backup">Create Backup</string>
<string name="create_account">Create Account</string>
<string name="edit_account">Edit Account</string>
<string name="alert">Alert</string>
<string name="copy_keypharse_msg">Copy This Key Phrase this will be used to recover this backup, this will not be provided by KeyPass Again, have you copied or written down?</string>
<string name="help">Help</string>
@@ -94,6 +96,7 @@
<string name="change_app_hint">Change Hint</string>
<string name="remove_app_hint">Remove Hint</string>
<string name="app_password_hint">App Password Hint</string>
<string name="app_password_hint_desc">A password hint helps you remember your password if you forget it. Make sure it\'s a hint only you would understand.</string>
<string name="set_app_password_hint">Set App Password Hint</string>
<string name="change_app_password_hint">Change App Password Hint</string>
<string name="old_password">Old Password</string>
@@ -144,6 +147,7 @@
<string name="contact_developer">Contact Developer</string>
<string name="contact_via_email">Send an email for support, feedback, or suggestions</string>
<string name="choose_default_password_length">Choose Default Password Length</string>
<string name="choose_default_password_length_desc">This length will be used as default when generating new passwords. Longer passwords provide better security.</string>
</resources>
+1
View File
@@ -30,6 +30,7 @@ plugins {
id("org.jetbrains.kotlin.plugin.serialization") version (Versions.kotlin)
id("androidx.baselineprofile") version "1.3.4" apply false
id("com.android.test") version "8.10.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version(Versions.kotlin) apply false
}