mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2026-01-06 16:41:33 -06:00
Setting page migrate to compose (#478)
This commit is contained in:
committed by
GitHub
parent
507be14e5a
commit
39e05530e6
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user