Setting page migrate to compose (#478)

This commit is contained in:
Yogesh Choudhary Paliyal
2023-04-30 14:27:20 +05:30
committed by GitHub
parent 507be14e5a
commit 39e05530e6
16 changed files with 594 additions and 484 deletions

View File

@@ -31,9 +31,14 @@
android:name=".ui.generate.GeneratePasswordActivity"
android:exported="true" />
<activity
android:name=".ui.auth.AuthenticationActivity"
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:exported="false"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity
android:name=".ui.nav.DashboardComposeActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@@ -44,15 +49,6 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="com.journeyapps.barcodescanner.CaptureActivity"
android:exported="false"
android:screenOrientation="portrait"
tools:replace="screenOrientation" />
<activity
android:name=".ui.nav.DashboardComposeActivity"
android:exported="false"
android:windowSoftInputMode="adjustPan" />
<!-- If you want to disable android.startup completely. -->
<provider

View File

@@ -0,0 +1,90 @@
package com.yogeshpaliyal.keypass.ui.auth
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.common.utils.getKeyPassPassword
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.auth.components.ButtonBar
import com.yogeshpaliyal.keypass.ui.auth.components.PasswordInputField
import com.yogeshpaliyal.keypass.ui.redux.AuthState
import com.yogeshpaliyal.keypass.ui.redux.NavigationAction
import kotlinx.coroutines.launch
import org.reduxkotlin.compose.rememberDispatcher
@Composable
fun AuthScreen(state: AuthState) {
val context = LocalContext.current
val dispatchAction = rememberDispatcher()
val coroutineScope = rememberCoroutineScope()
val (password, setPassword) = remember(state) {
mutableStateOf("")
}
val (passwordVisible, setPasswordVisible) = remember(state) { mutableStateOf(false) }
val (passwordError, setPasswordError) = remember(state, password) {
mutableStateOf<Int?>(null)
}
BackHandler(state is AuthState.ConfirmPassword) {
dispatchAction(NavigationAction(AuthState.CreatePassword))
}
LaunchedEffect(key1 = Unit, block = {
coroutineScope.launch {
val mPassword = context.getKeyPassPassword()
if (mPassword == null) {
dispatchAction(NavigationAction(AuthState.CreatePassword))
}
}
})
Column(
modifier = Modifier
.padding(32.dp)
.fillMaxSize(1f)
.verticalScroll(rememberScrollState()),
Arrangement.SpaceEvenly,
Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.ic_undraw_unlock_24mb),
contentDescription = ""
)
Text(text = stringResource(id = state.title))
PasswordInputField(
password,
setPassword,
passwordVisible,
setPasswordVisible,
passwordError
)
ButtonBar(state, password, setPasswordError) {
dispatchAction(it)
}
}
}

View File

@@ -1,215 +0,0 @@
package com.yogeshpaliyal.keypass.ui.auth
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.annotation.StringRes
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.yogeshpaliyal.common.utils.getKeyPassPassword
import com.yogeshpaliyal.common.utils.setKeyPassPassword
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.nav.DashboardComposeActivity
import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
private const val AUTHENTICATION_RESULT = 707
sealed class AuthState(@StringRes val title: Int) {
object CreatePassword : AuthState(R.string.create_password)
class ConfirmPassword(val password: String) : AuthState(R.string.confirm_password)
object Login : AuthState(R.string.login_to_enter_keypass)
}
@AndroidEntryPoint
class AuthenticationActivity : AppCompatActivity() {
@Preview(showSystemUi = true)
@Composable
fun AuthScreenPreview() {
AuthScreen()
}
@Composable
fun AuthScreen() {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val (state, setState) = remember {
mutableStateOf<AuthState>(AuthState.Login)
}
val (password, setPassword) = remember(state) {
mutableStateOf("")
}
val (passwordVisible, setPasswordVisible) = remember(state) { mutableStateOf(false) }
val (passwordError, setPasswordError) = remember(state, password) {
mutableStateOf<Int?>(null)
}
BackHandler(state is AuthState.ConfirmPassword) {
setState(AuthState.CreatePassword)
}
LaunchedEffect(key1 = Unit, block = {
coroutineScope.launch {
val mPassword = context.getKeyPassPassword()
if (mPassword == null) {
setState(AuthState.CreatePassword)
}
}
})
KeyPassTheme {
Column(
modifier = Modifier
.padding(32.dp)
.fillMaxSize(1f)
.verticalScroll(rememberScrollState()),
Arrangement.SpaceEvenly,
Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.ic_undraw_unlock_24mb),
contentDescription = ""
)
Text(text = stringResource(id = state.title))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(1f),
value = password,
singleLine = true,
placeholder = {
Text(text = stringResource(id = R.string.enter_password))
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
onValueChange = setPassword,
isError = passwordError != null,
supportingText = {
if (passwordError != null) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = passwordError),
color = MaterialTheme.colorScheme.error
)
}
},
trailingIcon = {
val image = if (passwordVisible) {
Icons.Rounded.Visibility
} else Icons.Rounded.VisibilityOff
// Please provide localized description for accessibility services
val description = if (passwordVisible) "Hide password" else "Show password"
IconButton(onClick = { setPasswordVisible(!passwordVisible) }) {
Icon(imageVector = image, description)
}
}
)
Row(modifier = Modifier.fillMaxWidth(1f), Arrangement.SpaceEvenly) {
AnimatedVisibility(state is AuthState.ConfirmPassword) {
Button(onClick = {
setState(AuthState.CreatePassword)
}) {
Text(text = stringResource(id = R.string.back))
}
}
Button(onClick = {
when (state) {
is AuthState.CreatePassword -> {
if (password.isBlank()) {
setPasswordError(R.string.enter_password)
} else {
setState(AuthState.ConfirmPassword(password))
}
}
is AuthState.ConfirmPassword -> {
if (state.password == password) {
coroutineScope.launch {
context.setKeyPassPassword(password)
onAuthComplete(context)
}
} else {
setPasswordError(R.string.password_no_match)
}
}
is AuthState.Login -> {
coroutineScope.launch {
val savedPassword = context.getKeyPassPassword()
if (savedPassword == password) {
onAuthComplete(context)
} else {
setPasswordError(R.string.incorrect_password)
}
}
}
}
}) {
Text(text = stringResource(id = R.string.str_continue))
}
}
}
}
}
private fun onAuthComplete(context: Context) {
// binding.passCodeView.isVisible = false
val dashboardIntent = Intent(context, DashboardComposeActivity::class.java)
startActivity(dashboardIntent)
finish()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
AuthScreen()
}
}
}

View File

@@ -0,0 +1,77 @@
package com.yogeshpaliyal.keypass.ui.auth.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.yogeshpaliyal.common.utils.getKeyPassPassword
import com.yogeshpaliyal.common.utils.setKeyPassPassword
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.redux.AuthState
import com.yogeshpaliyal.keypass.ui.redux.HomeState
import com.yogeshpaliyal.keypass.ui.redux.NavigationAction
import kotlinx.coroutines.launch
@Composable
fun ButtonBar(
state: AuthState,
password: String,
setPasswordError: (Int?) -> Unit,
dispatchAction: (NavigationAction) -> Unit
) {
val coroutineScope = rememberCoroutineScope()
val context = LocalContext.current
Row(modifier = Modifier.fillMaxWidth(1f), Arrangement.SpaceEvenly) {
AnimatedVisibility(state is AuthState.ConfirmPassword) {
Button(onClick = {
dispatchAction(NavigationAction(AuthState.CreatePassword))
}) {
Text(text = stringResource(id = R.string.back))
}
}
Button(onClick = {
when (state) {
is AuthState.CreatePassword -> {
if (password.isBlank()) {
setPasswordError(R.string.enter_password)
} else {
dispatchAction(NavigationAction(AuthState.ConfirmPassword(password)))
}
}
is AuthState.ConfirmPassword -> {
if (state.password == password) {
coroutineScope.launch {
context.setKeyPassPassword(password)
dispatchAction(NavigationAction(HomeState(), true))
}
} else {
setPasswordError(R.string.password_no_match)
}
}
is AuthState.Login -> {
coroutineScope.launch {
val savedPassword = context.getKeyPassPassword()
if (savedPassword == password) {
dispatchAction(NavigationAction(HomeState(), true))
} else {
setPasswordError(R.string.incorrect_password)
}
}
}
}
}) {
Text(text = stringResource(id = R.string.str_continue))
}
}
}

View File

@@ -0,0 +1,62 @@
package com.yogeshpaliyal.keypass.ui.auth.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import com.yogeshpaliyal.keypass.R
@Composable
fun PasswordInputField(
password: String,
setPassword: (String) -> Unit,
passwordVisible: Boolean,
setPasswordVisible: (Boolean) -> Unit,
passwordError: Int?
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(1f),
value = password,
singleLine = true,
placeholder = {
Text(text = stringResource(id = R.string.enter_password))
},
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
onValueChange = setPassword,
isError = passwordError != null,
supportingText = {
if (passwordError != null) {
Text(
modifier = Modifier.fillMaxWidth(),
text = stringResource(id = passwordError),
color = MaterialTheme.colorScheme.error
)
}
},
trailingIcon = {
val image = if (passwordVisible) {
Icons.Rounded.Visibility
} else Icons.Rounded.VisibilityOff
// Please provide localized description for accessibility services
val description = if (passwordVisible) "Hide password" else "Show password"
IconButton(onClick = { setPasswordVisible(!passwordVisible) }) {
Icon(imageVector = image, description)
}
}
)
}

View File

@@ -1,10 +1,13 @@
package com.yogeshpaliyal.keypass.ui.home
import android.app.Application
import android.content.ContentResolver
import android.net.Uri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.viewModelScope
import com.yogeshpaliyal.common.data.AccountModel
import com.yogeshpaliyal.common.dbhelper.restoreBackup
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -27,7 +30,12 @@ class DashboardViewModel @Inject constructor(
val mediator = MediatorLiveData<List<AccountModel>>()
fun queryUpdated(keyword: String?, tag: String?, sortField: String?, sortAscending: Boolean = true) {
fun queryUpdated(
keyword: String?,
tag: String?,
sortField: String?,
sortAscending: Boolean = true
) {
viewModelScope.launch(Dispatchers.IO) {
if (sortAscending) {
mediator.postValue(appDao.getAllAccountsAscending(keyword ?: "", tag, sortField))
@@ -36,4 +44,12 @@ class DashboardViewModel @Inject constructor(
}
}
}
suspend fun restoreBackup(
keyphrase: String,
contentResolver: ContentResolver,
fileUri: Uri?
): Boolean {
return appDb.restoreBackup(keyphrase, contentResolver, fileUri)
}
}

View File

@@ -1,5 +1,6 @@
package com.yogeshpaliyal.keypass.ui.home
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -67,14 +68,14 @@ fun Homepage(
)
}
if (tag != null) {
AnimatedVisibility(tag != null) {
LazyRow(
modifier = Modifier.padding(vertical = 8.dp),
contentPadding = PaddingValues(horizontal = 16.dp),
content = {
item {
AssistChip(onClick = { }, label = {
Text(text = tag)
Text(text = tag ?: "")
}, trailingIcon = {
IconButton(onClick = {
dispatchAction(NavigationAction(HomeState(), true))

View File

@@ -1,10 +1,13 @@
package com.yogeshpaliyal.keypass.ui.home.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -53,31 +56,35 @@ import kotlinx.coroutines.delay
import org.reduxkotlin.compose.rememberDispatcher
import kotlin.time.Duration.Companion.seconds
@OptIn(ExperimentalFoundationApi::class, ExperimentalAnimationApi::class)
@Composable
fun AccountsList(accounts: List<AccountModel>? = null) {
val dispatch = rememberDispatcher()
if (accounts?.isNotEmpty() == true) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(accounts) { account ->
Account(
account,
onClick = {
if (it.type == AccountType.TOTP) {
dispatch(IntentNavigation.AddTOTP(it.uniqueId))
} else {
dispatch(NavigationAction(AccountDetailState(it.id)))
AnimatedContent(targetState = accounts) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(it) { account ->
Account(
modifier = Modifier.animateItemPlacement(),
account,
onClick = {
if (it.type == AccountType.TOTP) {
dispatch(IntentNavigation.AddTOTP(it.uniqueId))
} else {
dispatch(NavigationAction(AccountDetailState(it.id)))
}
}
}
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
)
}
item {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
} else {
@@ -87,12 +94,14 @@ fun AccountsList(accounts: List<AccountModel>? = null) {
@Composable
fun Account(
modifier: Modifier,
accountModel: AccountModel,
onClick: (AccountModel) -> Unit
) {
val dispatch = rememberDispatcher()
Card(
modifier = modifier,
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp),
onClick = { onClick(accountModel) }
) {
@@ -218,7 +227,8 @@ fun WrapWithProgress(accountModel: AccountModel) {
return
}
val infiniteTransition = rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "")
val infiniteTransition =
rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "")
val rotationAnimation = infiniteTransition.animateFloat(
initialValue = 1f - (accountModel.getTOtpProgress().toFloat() / 30),
targetValue = 1f,

View File

@@ -1,5 +1,6 @@
package com.yogeshpaliyal.keypass.ui.home.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -40,7 +41,7 @@ fun SearchBar(
},
trailingIcon = {
Row {
if (keyword.isNullOrBlank().not()) {
AnimatedVisibility(keyword.isNullOrBlank().not()) {
IconButton(onClick = { updateKeyword("") }) {
Icon(
painter = rememberVectorPainter(image = Icons.Rounded.Close),

View File

@@ -34,6 +34,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
@@ -45,12 +46,12 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidViewBinding
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.databinding.LayoutMySettingsFragmentBinding
import com.yogeshpaliyal.keypass.ui.auth.AuthScreen
import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage
import com.yogeshpaliyal.keypass.ui.home.Homepage
import com.yogeshpaliyal.keypass.ui.redux.AccountDetailState
import com.yogeshpaliyal.keypass.ui.redux.AuthState
import com.yogeshpaliyal.keypass.ui.redux.BottomSheetAction
import com.yogeshpaliyal.keypass.ui.redux.BottomSheetState
import com.yogeshpaliyal.keypass.ui.redux.GoBackAction
@@ -62,6 +63,7 @@ import com.yogeshpaliyal.keypass.ui.redux.ScreenState
import com.yogeshpaliyal.keypass.ui.redux.SettingsState
import com.yogeshpaliyal.keypass.ui.redux.TotpDetailState
import com.yogeshpaliyal.keypass.ui.redux.UpdateContextAction
import com.yogeshpaliyal.keypass.ui.settings.MySettingCompose
import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme
import dagger.hilt.android.AndroidEntryPoint
import org.reduxkotlin.compose.StoreProvider
@@ -101,9 +103,11 @@ fun Dashboard() {
dispatch(GoBackAction)
}
if (systemBackPress) {
(context as? AppCompatActivity)?.onBackPressed()
}
LaunchedEffect(key1 = systemBackPress, block = {
if (systemBackPress) {
(context as? AppCompatActivity)?.onBackPressed()
}
})
DisposableEffect(KeyPassRedux, context) {
dispatch(UpdateContextAction(context))
@@ -128,27 +132,27 @@ fun Dashboard() {
fun CurrentPage() {
val currentScreen by selectState<KeyPassState, ScreenState> { this.currentScreen }
when (currentScreen) {
is HomeState -> {
Homepage(homeState = (currentScreen as HomeState))
}
currentScreen.let {
when (it) {
is HomeState -> {
Homepage(homeState = it)
}
is SettingsState -> {
MySettings()
}
is SettingsState -> {
MySettingCompose()
}
is AccountDetailState -> {
AccountDetailPage(id = (currentScreen as AccountDetailState).accountId)
}
is AccountDetailState -> {
AccountDetailPage(id = it.accountId)
}
is TotpDetailState -> {
}
}
}
is AuthState -> {
AuthScreen(it)
}
@Composable
fun MySettings() {
AndroidViewBinding(LayoutMySettingsFragmentBinding::inflate) {
is TotpDetailState -> {
}
}
}
}
@@ -156,9 +160,9 @@ fun MySettings() {
fun OptionBottomBar(
viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
) {
val bottomSheetState by selectState<KeyPassState, BottomSheetState> { this.bottomSheet }
val bottomSheetState by selectState<KeyPassState, BottomSheetState?> { this.bottomSheet }
if (!bottomSheetState.isBottomSheetOpen) {
if (bottomSheetState?.isBottomSheetOpen != true) {
return
}
@@ -230,7 +234,9 @@ fun NavItem(item: NavigationModelItem.NavMenuItem, onClick: () -> Unit) {
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = true),
onClick = onClick
).padding(16.dp).fillMaxWidth(1f)
)
.padding(16.dp)
.fillMaxWidth(1f)
) {
Icon(
painter = painterResource(id = item.icon),

View File

@@ -15,6 +15,8 @@ data class StateUpdateAction(val state: ScreenState) : Action
sealed interface IntentNavigation : Action {
object GeneratePassword : IntentNavigation
object BackupActivity : IntentNavigation
object ShareApp : IntentNavigation
data class AddTOTP(val accountId: String? = null) : IntentNavigation
}

View File

@@ -5,8 +5,10 @@ import android.content.ClipboardManager
import android.content.Intent
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.ui.addTOTP.AddTOTPActivity
import com.yogeshpaliyal.keypass.ui.backup.BackupActivity
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity
import org.reduxkotlin.Reducer
import org.reduxkotlin.applyMiddleware
@@ -56,6 +58,13 @@ object KeyPassRedux {
state.copy(context = action.context)
}
is ToastAction -> {
state.context?.let {
Toast.makeText(it, action.text, Toast.LENGTH_SHORT).show()
}
state
}
is GoBackAction -> {
val lastItem = arrPages.removeLastOrNull()
if (lastItem != null) {
@@ -91,6 +100,21 @@ object KeyPassRedux {
is IntentNavigation.AddTOTP -> {
AddTOTPActivity.start(state.context, action.accountId)
}
is IntentNavigation.BackupActivity -> {
BackupActivity.start(state.context)
}
is IntentNavigation.ShareApp -> {
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(
Intent.EXTRA_TEXT,
"KeyPass Password Manager\n Offline, Secure, Open Source https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID
)
sendIntent.type = "text/plain"
state.context?.startActivity(Intent.createChooser(sendIntent, state.context.getString(R.string.share_keypass)))
}
}
next(action)
}

View File

@@ -2,11 +2,13 @@ package com.yogeshpaliyal.keypass.ui.redux
import android.content.Context
import android.os.Bundle
import androidx.annotation.StringRes
import com.yogeshpaliyal.keypass.R
data class KeyPassState(
val context: Context? = null,
val currentScreen: ScreenState,
val bottomSheet: BottomSheetState,
val bottomSheet: BottomSheetState? = null,
val systemBackPress: Boolean = false
)
@@ -17,6 +19,11 @@ data class AccountDetailState(val accountId: Long? = null) : ScreenState()
data class TotpDetailState(val accountId: String? = null) : ScreenState()
object SettingsState : ScreenState(true)
open class AuthState(@StringRes val title: Int) : ScreenState(false) {
object CreatePassword : AuthState(R.string.create_password)
class ConfirmPassword(val password: String) : AuthState(R.string.confirm_password)
object Login : AuthState(R.string.login_to_enter_keypass)
}
data class BottomSheetState(
val path: String,
val args: Bundle? = null,
@@ -24,7 +31,7 @@ data class BottomSheetState(
)
fun generateDefaultState(): KeyPassState {
val currentPage = HomeState()
val currentPage = AuthState.Login
val bottomSheet = BottomSheetState(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen = false)
return KeyPassState(currentScreen = currentPage, bottomSheet = bottomSheet)
}

View File

@@ -1,39 +1,59 @@
package com.yogeshpaliyal.keypass.ui.settings
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentResolver
import android.content.DialogInterface
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.content.ContextCompat.getSystemService
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
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.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Feedback
import androidx.compose.material.icons.rounded.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Divider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.preference.PreferenceFragmentCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.yogeshpaliyal.common.dbhelper.createBackup
import com.yogeshpaliyal.common.dbhelper.restoreBackup
import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
import com.yogeshpaliyal.common.utils.email
import com.yogeshpaliyal.common.utils.getOrCreateBackupKey
import com.yogeshpaliyal.common.utils.setBackupDirectory
import com.yogeshpaliyal.keypass.BuildConfig
import com.yogeshpaliyal.keypass.R
import com.yogeshpaliyal.keypass.databinding.LayoutBackupKeypharseBinding
import com.yogeshpaliyal.keypass.databinding.LayoutRestoreKeypharseBinding
import com.yogeshpaliyal.keypass.ui.backup.BackupActivity
import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel
import com.yogeshpaliyal.keypass.ui.redux.Action
import com.yogeshpaliyal.keypass.ui.redux.IntentNavigation
import com.yogeshpaliyal.keypass.ui.redux.ToastAction
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.reduxkotlin.compose.rememberTypedDispatcher
import javax.inject.Inject
private const val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212
private const val CHOOSE_RESTORE_FILE_REQUEST_CODE = 26213
@AndroidEntryPoint
class MySettingsFragment : PreferenceFragmentCompat() {
@@ -43,202 +63,181 @@ class MySettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.preferences, rootKey)
}
}
override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when (preference.key) {
"feedback" -> {
context?.email(
getString(R.string.feedback_to_keypass),
"yogeshpaliyal.foss@gmail.com"
)
true
}
"backup" -> {
BackupActivity.start(context)
true
}
getString(R.string.settings_restore_backup) -> {
selectRestoreFile()
true
}
"share" -> {
val sendIntent = Intent()
sendIntent.action = Intent.ACTION_SEND
sendIntent.putExtra(
Intent.EXTRA_TEXT,
"KeyPass Password Manager\n Offline, Secure, Open Source https://play.google.com/store/apps/details?id=" + BuildConfig.APPLICATION_ID
)
sendIntent.type = "text/plain"
startActivity(Intent.createChooser(sendIntent, getString(R.string.share_keypass)))
true
}
else -> super.onPreferenceTreeClick(preference)
}
@Composable
fun RestoreDialog(
selectedFile: Uri,
hideDialog: () -> Unit,
mViewModel: DashboardViewModel = hiltViewModel()
) {
val (keyphrase, setKeyPhrase) = remember {
mutableStateOf("")
}
private fun selectRestoreFile() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
val dispatchAction = rememberTypedDispatcher<Action>()
intent.addFlags(
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
AlertDialog(
onDismissRequest = {
hideDialog()
},
title = {
Text(text = stringResource(id = R.string.restore))
},
confirmButton = {
TextButton(onClick = {
if (keyphrase.isEmpty()) {
dispatchAction(ToastAction(R.string.alert_blank_keyphrase))
return@TextButton
}
if (keyphrase.length != BACKUP_KEY_LENGTH) {
dispatchAction(ToastAction(R.string.alert_invalid_keyphrase))
return@TextButton
}
coroutineScope.launch {
val result =
mViewModel.restoreBackup(keyphrase, context.contentResolver, selectedFile)
if (result) {
hideDialog()
dispatchAction(ToastAction(R.string.backup_restored))
} else {
dispatchAction(ToastAction(R.string.invalid_keyphrase))
}
}
}) {
Text(text = stringResource(id = R.string.restore))
}
},
dismissButton = {
TextButton(onClick = hideDialog) {
Text(text = stringResource(id = R.string.cancel))
}
},
text = {
Column(modifier = Modifier.fillMaxWidth(1f)) {
Text(text = stringResource(id = R.string.keyphrase_restore_info))
Spacer(modifier = Modifier.size(8.dp))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(1f),
value = keyphrase,
onValueChange = setKeyPhrase,
placeholder = {
Text(text = stringResource(id = R.string.enter_keyphrase))
}
)
}
}
)
}
@Preview(showSystemUi = true)
@Composable
fun MySettingCompose() {
val dispatchAction = rememberTypedDispatcher<Action>()
val context = LocalContext.current
val (result, setResult) = remember { mutableStateOf<Uri?>(null) }
val launcher = rememberLauncherForActivityResult(OpenKeyPassBackup()) {
setResult(it)
}
result?.let {
RestoreDialog(
selectedFile = it,
hideDialog = {
setResult(null)
}
)
try {
startActivityForResult(intent, CHOOSE_RESTORE_FILE_REQUEST_CODE)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == CHOOSE_BACKUPS_LOCATION_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val contentResolver = context?.contentResolver
val selectedDirectory = data?.data
if (contentResolver != null && selectedDirectory != null) {
contentResolver.takePersistableUriPermission(
selectedDirectory,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
lifecycleScope.launch {
context?.setBackupDirectory(selectedDirectory.toString())
backup(selectedDirectory)
}
}
} else if (requestCode == CHOOSE_RESTORE_FILE_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
val contentResolver = context?.contentResolver
val selectedFile = data?.data
if (contentResolver != null && selectedFile != null) {
val binding = LayoutRestoreKeypharseBinding.inflate(layoutInflater)
val dialog = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
.setNegativeButton(
R.string.cancel
) { dialog, which ->
dialog.dismiss()
}
.setPositiveButton(
R.string.restore
) { dialog, which ->
dialog.dismiss()
}.create()
dialog.setOnShowListener {
val positiveBtn = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
positiveBtn.setOnClickListener {
restore(
dialog,
binding.etKeyPhrase.text.toString(),
contentResolver,
selectedFile
)
}
}
dialog.show()
}
Column {
PreferenceItem(title = R.string.security, isCategory = true)
PreferenceItem(
title = R.string.credentials_backups,
summary = R.string.credentials_backups_desc
) {
dispatchAction(IntentNavigation.BackupActivity)
}
PreferenceItem(
title = R.string.restore_credentials,
summary = R.string.restore_credentials_desc
) {
launcher.launch(arrayOf())
}
Divider(
modifier = Modifier
.fillMaxWidth(1f)
.height(1.dp)
)
PreferenceItem(title = R.string.help, isCategory = true)
PreferenceItem(
title = R.string.send_feedback,
summary = R.string.send_feedback_desc,
icon = Icons.Rounded.Feedback
) {
context.email(
context.getString(R.string.feedback_to_keypass),
"yogeshpaliyal.foss@gmail.com"
)
}
PreferenceItem(
title = R.string.share,
summary = R.string.share_desc,
icon = Icons.Rounded.Share
) {
dispatchAction(IntentNavigation.ShareApp)
}
}
}
private fun restore(
dialog: AlertDialog,
keyphrase: String,
contentResolver: ContentResolver,
selectedFile: Uri
@Composable
fun PreferenceItem(
@StringRes title: Int,
@StringRes summary: Int? = null,
icon: ImageVector? = null,
isCategory: Boolean = false,
onClickItem: (() -> Unit)? = null
) {
val titleColor = if (isCategory) {
MaterialTheme.colorScheme.secondary
} else {
Color.Unspecified
}
Row(
modifier = Modifier
.fillMaxWidth(1f)
.widthIn(48.dp)
.padding(horizontal = 16.dp)
.clickable(onClickItem != null) {
onClickItem?.invoke()
},
verticalAlignment = Alignment.CenterVertically
) {
if (keyphrase.isEmpty()) {
Toast.makeText(
context,
R.string.alert_blank_keyphrase,
Toast.LENGTH_SHORT
)
.show()
return
}
if (keyphrase.length != BACKUP_KEY_LENGTH) {
Toast.makeText(
context,
R.string.alert_invalid_keyphrase,
Toast.LENGTH_SHORT
).show()
return
}
lifecycleScope.launch {
val result = appDb.restoreBackup(
keyphrase,
contentResolver,
selectedFile
)
if (result) {
dialog.dismiss()
Toast.makeText(
context,
getString(R.string.backup_restored),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
getString(R.string.invalid_keyphrase),
Toast.LENGTH_SHORT
).show()
Box(modifier = Modifier.width(56.dp), Alignment.CenterStart) {
if (icon != null) {
Icon(painter = rememberVectorPainter(image = icon), contentDescription = "")
}
}
}
suspend fun backup(selectedDirectory: Uri) {
val keyPair = requireContext().getOrCreateBackupKey()
val tempFile = DocumentFile.fromTreeUri(requireContext(), selectedDirectory)?.createFile(
"*/*",
"key_pass_backup_${System.currentTimeMillis()}.keypass"
)
lifecycleScope.launch {
context?.contentResolver?.let {
appDb.createBackup(
keyPair.second,
it,
tempFile?.uri
)
if (keyPair.first) {
val binding = LayoutBackupKeypharseBinding.inflate(layoutInflater)
binding.txtCode.text = requireContext().getOrCreateBackupKey().second
binding.txtCode.setOnClickListener {
val clipboard =
getSystemService(requireContext(), ClipboardManager::class.java)
val clip = ClipData.newPlainText(
getString(R.string.app_name),
binding.txtCode.text
)
clipboard?.setPrimaryClip(clip)
Toast.makeText(
context,
getString(R.string.copied_to_clipboard),
Toast.LENGTH_SHORT
).show()
}
MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
.setPositiveButton(
"Yes"
) { dialog, which ->
dialog?.dismiss()
}.show()
} else {
Toast.makeText(
context,
getString(R.string.backup_completed),
Toast.LENGTH_SHORT
).show()
}
Column(
modifier = Modifier
.padding(vertical = 16.dp)
.fillMaxWidth(1f)
) {
Text(
text = stringResource(id = title),
color = titleColor,
style = TextStyle(fontSize = 16.sp)
)
if (summary != null) {
Text(text = stringResource(id = summary), style = TextStyle(fontSize = 14.sp))
}
}
}

View File

@@ -0,0 +1,14 @@
package com.yogeshpaliyal.keypass.ui.settings
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
@HiltViewModel
class MySettingsViewModel @Inject constructor(
application: Application,
val appDb: com.yogeshpaliyal.common.AppDatabase
) : AndroidViewModel(application) {
private val appDao = appDb.getDao()
}

View File

@@ -0,0 +1,20 @@
package com.yogeshpaliyal.keypass.ui.settings
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContracts
class OpenKeyPassBackup : ActivityResultContracts.OpenDocument() {
override fun createIntent(context: Context, input: Array<String>): Intent {
super.createIntent(context, input)
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "*/*"
intent.addFlags(
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
return intent
}
}