mirror of
https://github.com/yogeshpaliyal/KeyPass.git
synced 2026-01-06 09:09:44 -06:00
Backup Screen Migration to Compose (#512)
This commit is contained in:
committed by
GitHub
parent
b3c94f1b04
commit
0e65479a85
@@ -1,14 +1,14 @@
|
||||
package com.yogeshpaliyal.keypass
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
// A custom runner to set up the instrumented application class for tests.
|
||||
class CustomTestRunner : AndroidJUnitRunner() {
|
||||
|
||||
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.runner.AndroidJUnitRunner
|
||||
import dagger.hilt.android.testing.HiltTestApplication
|
||||
|
||||
// A custom runner to set up the instrumented application class for tests.
|
||||
class CustomTestRunner : AndroidJUnitRunner() {
|
||||
|
||||
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
package com.yogeshpaliyal.keypass
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.yogeshpaliyal.keypass", appContext.packageName)
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("com.yogeshpaliyal.keypass", appContext.packageName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,110 +1,106 @@
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.test.espresso.Espresso.onView
|
||||
import androidx.test.espresso.action.ViewActions.click
|
||||
import androidx.test.espresso.matcher.ViewMatchers.withId
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class DashboardActivityTest {
|
||||
@get:Rule(order = 0)
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
var activityScenarioRule = createAndroidComposeRule<DashboardActivity>()
|
||||
|
||||
@Inject
|
||||
lateinit var appDatabase: com.yogeshpaliyal.common.AppDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
appDatabase.clearAllTables()
|
||||
}
|
||||
|
||||
private fun getDummyAccount(): AccountModel {
|
||||
val accountModel = AccountModel()
|
||||
accountModel.title = "Github ${System.currentTimeMillis()}"
|
||||
accountModel.username = "yogeshpaliyal"
|
||||
accountModel.password = "1234567890"
|
||||
accountModel.tags = "social"
|
||||
accountModel.site = "https://yogeshpaliyal.com"
|
||||
accountModel.notes = "Testing Notes"
|
||||
return accountModel
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addAccountAndDetailAndDeleteTest() {
|
||||
val accountModel = getDummyAccount()
|
||||
addAccount(accountModel)
|
||||
checkAccountDetail(accountModel)
|
||||
deleteAccount(accountModel)
|
||||
}
|
||||
|
||||
private fun addAccount(accountModel: AccountModel) {
|
||||
// Navigate to add screen
|
||||
onView(withId(R.id.btnAdd)).perform(click())
|
||||
|
||||
// Fill information on Detail Activity
|
||||
activityScenarioRule.onNodeWithTag("accountName").performTextInput(accountModel.title ?: "")
|
||||
|
||||
// generate random password
|
||||
activityScenarioRule.onNodeWithTag("username").performTextInput(accountModel.username ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("password").performTextInput(accountModel.password ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("tags").performTextInput(accountModel.tags ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("website").performTextInput(accountModel.site ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("notes").performTextInput(accountModel.notes ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("save").performClick()
|
||||
|
||||
// is showing in listing
|
||||
activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertIsDisplayed()
|
||||
}
|
||||
|
||||
private fun checkAccountDetail(accountModel: AccountModel) {
|
||||
// Navigate to account detail
|
||||
activityScenarioRule.onNodeWithText(accountModel.username ?: "").performClick()
|
||||
|
||||
// Fill information on Detail Activity
|
||||
activityScenarioRule.onNodeWithTag("accountName").assertTextEquals(accountModel.title ?: "")
|
||||
|
||||
// generate random password
|
||||
activityScenarioRule.onNodeWithTag("username").assertTextEquals(accountModel.username ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("password").assertTextEquals(accountModel.password ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("tags").assertTextEquals(accountModel.tags ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("website").assertTextEquals(accountModel.site ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("notes").assertTextEquals(accountModel.notes ?: "")
|
||||
}
|
||||
|
||||
private fun deleteAccount(accountModel: AccountModel) {
|
||||
// delete account
|
||||
activityScenarioRule.onNodeWithTag("action_delete").performClick()
|
||||
|
||||
activityScenarioRule.onNodeWithTag("delete").performClick()
|
||||
|
||||
// is not showing in listing
|
||||
activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.assertTextEquals
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onNodeWithTag
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class DashboardActivityTest {
|
||||
@get:Rule(order = 0)
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule(order = 1)
|
||||
var activityScenarioRule = createAndroidComposeRule<DashboardComposeActivity>()
|
||||
|
||||
@Inject
|
||||
lateinit var appDatabase: com.yogeshpaliyal.common.AppDatabase
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
appDatabase.clearAllTables()
|
||||
}
|
||||
|
||||
private fun getDummyAccount(): AccountModel {
|
||||
val accountModel = AccountModel()
|
||||
accountModel.title = "Github ${System.currentTimeMillis()}"
|
||||
accountModel.username = "yogeshpaliyal"
|
||||
accountModel.password = "1234567890"
|
||||
accountModel.tags = "social"
|
||||
accountModel.site = "https://yogeshpaliyal.com"
|
||||
accountModel.notes = "Testing Notes"
|
||||
return accountModel
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addAccountAndDetailAndDeleteTest() {
|
||||
val accountModel = getDummyAccount()
|
||||
addAccount(accountModel)
|
||||
checkAccountDetail(accountModel)
|
||||
deleteAccount(accountModel)
|
||||
}
|
||||
|
||||
private fun addAccount(accountModel: AccountModel) {
|
||||
// Navigate to add screen
|
||||
activityScenarioRule.onNodeWithTag("btnAdd").performClick()
|
||||
|
||||
// Fill information on Detail Activity
|
||||
activityScenarioRule.onNodeWithTag("accountName").performTextInput(accountModel.title ?: "")
|
||||
|
||||
// generate random password
|
||||
activityScenarioRule.onNodeWithTag("username").performTextInput(accountModel.username ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("password").performTextInput(accountModel.password ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("tags").performTextInput(accountModel.tags ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("website").performTextInput(accountModel.site ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("notes").performTextInput(accountModel.notes ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("save").performClick()
|
||||
|
||||
// is showing in listing
|
||||
activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertIsDisplayed()
|
||||
}
|
||||
|
||||
private fun checkAccountDetail(accountModel: AccountModel) {
|
||||
// Navigate to account detail
|
||||
activityScenarioRule.onNodeWithText(accountModel.username ?: "").performClick()
|
||||
|
||||
// Fill information on Detail Activity
|
||||
activityScenarioRule.onNodeWithTag("accountName").assertTextEquals(accountModel.title ?: "")
|
||||
|
||||
// generate random password
|
||||
activityScenarioRule.onNodeWithTag("username").assertTextEquals(accountModel.username ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("password").assertTextEquals(accountModel.password ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("tags").assertTextEquals(accountModel.tags ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("website").assertTextEquals(accountModel.site ?: "")
|
||||
|
||||
activityScenarioRule.onNodeWithTag("notes").assertTextEquals(accountModel.notes ?: "")
|
||||
}
|
||||
|
||||
private fun deleteAccount(accountModel: AccountModel) {
|
||||
// delete account
|
||||
activityScenarioRule.onNodeWithTag("action_delete").performClick()
|
||||
|
||||
activityScenarioRule.onNodeWithTag("delete").performClick()
|
||||
|
||||
// is not showing in listing
|
||||
activityScenarioRule.onNodeWithText(accountModel.username ?: "").assertDoesNotExist()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.yogeshpaliyal.keypass
|
||||
|
||||
import android.content.Intent
|
||||
import com.yogeshpaliyal.common.CommonMyApplication
|
||||
import com.yogeshpaliyal.keypass.ui.CrashActivity
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* yogeshpaliyal.foss@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 22-01-2021 22:41
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class MyApplication : CommonMyApplication() {
|
||||
|
||||
override fun getCrashActivityIntent(throwable: Throwable): Intent {
|
||||
return CrashActivity.getIntent(this, throwable.stackTraceToString())
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass
|
||||
|
||||
import android.content.Intent
|
||||
import com.yogeshpaliyal.common.CommonMyApplication
|
||||
import com.yogeshpaliyal.keypass.ui.CrashActivity
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* yogeshpaliyal.foss@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 22-01-2021 22:41
|
||||
*/
|
||||
@HiltAndroidApp
|
||||
class MyApplication : CommonMyApplication() {
|
||||
|
||||
override fun getCrashActivityIntent(throwable: Throwable): Intent {
|
||||
return CrashActivity.getIntent(this, throwable.stackTraceToString())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.yogeshpaliyal.keypass.customViews
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Path
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.shape.ShapeAppearanceModel
|
||||
import com.google.android.material.shape.ShapeAppearancePathProvider
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
|
||||
/**
|
||||
* A Card view that clips the content of any shape, this should be done upstream in card,
|
||||
* working around it for now.
|
||||
*/
|
||||
class MaskedCardView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = R.attr.materialCardViewStyle
|
||||
) : MaterialCardView(context, attrs, defStyle) {
|
||||
@SuppressLint("RestrictedApi")
|
||||
private val pathProvider = ShapeAppearancePathProvider()
|
||||
private val path: Path = Path()
|
||||
private val shapeAppearance: ShapeAppearanceModel = ShapeAppearanceModel.builder(
|
||||
context,
|
||||
attrs,
|
||||
defStyle,
|
||||
R.style.Widget_MaterialComponents_CardView
|
||||
).build()
|
||||
|
||||
private val rectF = RectF(0f, 0f, 0f, 0f)
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
canvas.clipPath(path)
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
rectF.right = w.toFloat()
|
||||
rectF.bottom = h.toFloat()
|
||||
pathProvider.calculatePath(shapeAppearance, 1f, rectF, path)
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.yogeshpaliyal.keypass.listener
|
||||
|
||||
import android.view.View
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 09:00
|
||||
*/
|
||||
interface AccountsClickListener<T> {
|
||||
fun onItemClick(view: View, model: T)
|
||||
fun onCopyClicked(model: T)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.yogeshpaliyal.keypass.listener
|
||||
|
||||
import android.view.View
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 09:00
|
||||
*/
|
||||
interface UniversalClickListener<T> {
|
||||
fun onItemClick(view: View, model: T)
|
||||
}
|
||||
@@ -1,65 +1,65 @@
|
||||
package com.yogeshpaliyal.keypass.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.yogeshpaliyal.keypass.BuildConfig
|
||||
import com.yogeshpaliyal.keypass.databinding.ActivityCrashBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.StringBuilder
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CrashActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityCrashBinding
|
||||
|
||||
companion object {
|
||||
private const val ARG_DATA = "arg_data"
|
||||
|
||||
fun getIntent(context: Context, data: String?): Intent {
|
||||
return Intent(context, CrashActivity::class.java).also {
|
||||
it.putExtra(ARG_DATA, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.txtCrash.text = intent.extras?.getString(ARG_DATA)
|
||||
|
||||
binding.btnSendFeedback.setOnClickListener {
|
||||
val deviceInfo = StringBuilder()
|
||||
deviceInfo.append(binding.txtCrash.text.toString())
|
||||
try {
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("App Version: " + BuildConfig.VERSION_NAME)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Brand Name: " + Build.BRAND)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Manufacturer Name: " + Build.MANUFACTURER)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Device Name: " + Build.MODEL)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Device API Version: " + Build.VERSION.SDK_INT)
|
||||
deviceInfo.append("\n")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SENDTO)
|
||||
intent.data = Uri.parse("mailto:")
|
||||
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("yogeshpaliyal.foss@gmail.com"))
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "Crash Report in KeyPass")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, deviceInfo.toString())
|
||||
|
||||
startActivity(Intent.createChooser(intent, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.yogeshpaliyal.keypass.BuildConfig
|
||||
import com.yogeshpaliyal.keypass.databinding.ActivityCrashBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import java.lang.StringBuilder
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CrashActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityCrashBinding
|
||||
|
||||
companion object {
|
||||
private const val ARG_DATA = "arg_data"
|
||||
|
||||
fun getIntent(context: Context, data: String?): Intent {
|
||||
return Intent(context, CrashActivity::class.java).also {
|
||||
it.putExtra(ARG_DATA, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityCrashBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
binding.txtCrash.text = intent.extras?.getString(ARG_DATA)
|
||||
|
||||
binding.btnSendFeedback.setOnClickListener {
|
||||
val deviceInfo = StringBuilder()
|
||||
deviceInfo.append(binding.txtCrash.text.toString())
|
||||
try {
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("App Version: " + BuildConfig.VERSION_NAME)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Brand Name: " + Build.BRAND)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Manufacturer Name: " + Build.MANUFACTURER)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Device Name: " + Build.MODEL)
|
||||
deviceInfo.append("\n")
|
||||
deviceInfo.append("Device API Version: " + Build.VERSION.SDK_INT)
|
||||
deviceInfo.append("\n")
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val intent = Intent(Intent.ACTION_SENDTO)
|
||||
intent.data = Uri.parse("mailto:")
|
||||
|
||||
intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("yogeshpaliyal.foss@gmail.com"))
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, "Crash Report in KeyPass")
|
||||
intent.putExtra(Intent.EXTRA_TEXT, deviceInfo.toString())
|
||||
|
||||
startActivity(Intent.createChooser(intent, ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,140 +1,140 @@
|
||||
package com.yogeshpaliyal.keypass.ui.addTOTP
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.yogeshpaliyal.common.utils.TOTPHelper
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.databinding.ActivityAddTotpactivityBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AddTOTPActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_ACCOUNT_ID = "account_id"
|
||||
|
||||
@JvmStatic
|
||||
fun start(context: Context?, accountId: String? = null) {
|
||||
val starter = Intent(context, AddTOTPActivity::class.java)
|
||||
starter.putExtra(ARG_ACCOUNT_ID, accountId)
|
||||
context?.startActivity(starter)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityAddTotpactivityBinding
|
||||
|
||||
private val mViewModel by viewModels<AddTOTPViewModel>()
|
||||
|
||||
private val accountId by lazy {
|
||||
intent.extras?.getString(ARG_ACCOUNT_ID)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAddTotpactivityBinding.inflate(layoutInflater)
|
||||
binding.mViewModel = mViewModel
|
||||
binding.lifecycleOwner = this
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.tilSecretKey.isVisible = accountId == null
|
||||
mViewModel.loadOldAccount(accountId)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
binding.tilSecretKey.setEndIconOnClickListener {
|
||||
// ScannerActivity.start(this)
|
||||
IntentIntegrator(this).setPrompt("").initiateScan()
|
||||
}
|
||||
|
||||
mViewModel.error.observe(
|
||||
this,
|
||||
Observer {
|
||||
it?.getContentIfNotHandled()?.let {
|
||||
Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mViewModel.goBack.observe(
|
||||
this,
|
||||
Observer {
|
||||
it.getContentIfNotHandled()?.let {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
mViewModel.saveAccount(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
if (accountId != null) {
|
||||
menuInflater.inflate(R.menu.menu_delete, menu)
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_delete) {
|
||||
deleteAccount()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun deleteAccount() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.delete_account_title))
|
||||
.setMessage(getString(R.string.delete_account_msg))
|
||||
.setPositiveButton(
|
||||
getString(R.string.delete)
|
||||
) { dialog, which ->
|
||||
dialog?.dismiss()
|
||||
|
||||
if (accountId != null) {
|
||||
mViewModel.deleteAccount(accountId!!) {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
}.show()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (result != null) {
|
||||
if (result.contents != null) {
|
||||
try {
|
||||
val totp = TOTPHelper(result.contents)
|
||||
mViewModel.setSecretKey(totp.secret)
|
||||
mViewModel.setAccountName(totp.label)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.addTOTP
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.Observer
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
import com.yogeshpaliyal.common.utils.TOTPHelper
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.databinding.ActivityAddTotpactivityBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AddTOTPActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ARG_ACCOUNT_ID = "account_id"
|
||||
|
||||
@JvmStatic
|
||||
fun start(context: Context?, accountId: String? = null) {
|
||||
val starter = Intent(context, AddTOTPActivity::class.java)
|
||||
starter.putExtra(ARG_ACCOUNT_ID, accountId)
|
||||
context?.startActivity(starter)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityAddTotpactivityBinding
|
||||
|
||||
private val mViewModel by viewModels<AddTOTPViewModel>()
|
||||
|
||||
private val accountId by lazy {
|
||||
intent.extras?.getString(ARG_ACCOUNT_ID)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityAddTotpactivityBinding.inflate(layoutInflater)
|
||||
binding.mViewModel = mViewModel
|
||||
binding.lifecycleOwner = this
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.tilSecretKey.isVisible = accountId == null
|
||||
mViewModel.loadOldAccount(accountId)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
binding.tilSecretKey.setEndIconOnClickListener {
|
||||
// ScannerActivity.start(this)
|
||||
IntentIntegrator(this).setPrompt("").initiateScan()
|
||||
}
|
||||
|
||||
mViewModel.error.observe(
|
||||
this,
|
||||
Observer {
|
||||
it?.getContentIfNotHandled()?.let {
|
||||
Snackbar.make(binding.root, it, Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
mViewModel.goBack.observe(
|
||||
this,
|
||||
Observer {
|
||||
it.getContentIfNotHandled()?.let {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
binding.btnSave.setOnClickListener {
|
||||
mViewModel.saveAccount(accountId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
if (accountId != null) {
|
||||
menuInflater.inflate(R.menu.menu_delete, menu)
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.action_delete) {
|
||||
deleteAccount()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun deleteAccount() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.delete_account_title))
|
||||
.setMessage(getString(R.string.delete_account_msg))
|
||||
.setPositiveButton(
|
||||
getString(R.string.delete)
|
||||
) { dialog, which ->
|
||||
dialog?.dismiss()
|
||||
|
||||
if (accountId != null) {
|
||||
mViewModel.deleteAccount(accountId!!) {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, which ->
|
||||
dialog.dismiss()
|
||||
}.show()
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
val result = IntentIntegrator.parseActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (result != null) {
|
||||
if (result.contents != null) {
|
||||
try {
|
||||
val totp = TOTPHelper(result.contents)
|
||||
mViewModel.setSecretKey(totp.secret)
|
||||
mViewModel.setAccountName(totp.label)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,85 +1,85 @@
|
||||
package com.yogeshpaliyal.keypass.ui.addTOTP
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yogeshpaliyal.common.AppDatabase
|
||||
import com.yogeshpaliyal.common.constants.AccountType
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.common.utils.Event
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AddTOTPViewModel @Inject constructor(private val appDatabase: AppDatabase) :
|
||||
ViewModel() {
|
||||
|
||||
private val _goBack = MutableLiveData<Event<Unit>>()
|
||||
val goBack: LiveData<Event<Unit>> = _goBack
|
||||
|
||||
private val _error = MutableLiveData<Event<Int>>()
|
||||
val error: LiveData<Event<Int>> = _error
|
||||
|
||||
val secretKey = MutableLiveData<String>("")
|
||||
|
||||
val accountName = MutableLiveData<String>("")
|
||||
|
||||
fun loadOldAccount(accountId: String?) {
|
||||
accountId ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
appDatabase.getDao().getAccount(accountId)?.let { accountModel ->
|
||||
accountName.postValue(accountModel.title ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAccount(accountId: String?) {
|
||||
viewModelScope.launch {
|
||||
val secretKey = secretKey.value
|
||||
val accountName = accountName.value
|
||||
|
||||
if (accountId == null) {
|
||||
if (secretKey.isNullOrEmpty()) {
|
||||
_error.postValue(Event(R.string.alert_black_secret_key))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
if (accountName.isNullOrEmpty()) {
|
||||
_error.postValue(Event(R.string.alert_black_account_name))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val accountModel = if (accountId == null) {
|
||||
AccountModel(password = secretKey, title = accountName, type = AccountType.TOTP)
|
||||
} else {
|
||||
appDatabase.getDao().getAccount(accountId)?.also {
|
||||
it.title = accountName
|
||||
}
|
||||
}
|
||||
|
||||
accountModel?.let { appDatabase.getDao().insertOrUpdateAccount(it) }
|
||||
_goBack.postValue(Event(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
fun setSecretKey(secretKey: String) {
|
||||
this.secretKey.value = secretKey
|
||||
}
|
||||
|
||||
fun setAccountName(accountName: String) {
|
||||
this.accountName.value = accountName
|
||||
}
|
||||
|
||||
fun deleteAccount(accountId: String, onDeleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
appDatabase.getDao().deleteAccount(accountId)
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.addTOTP
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yogeshpaliyal.common.AppDatabase
|
||||
import com.yogeshpaliyal.common.constants.AccountType
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.common.utils.Event
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AddTOTPViewModel @Inject constructor(private val appDatabase: AppDatabase) :
|
||||
ViewModel() {
|
||||
|
||||
private val _goBack = MutableLiveData<Event<Unit>>()
|
||||
val goBack: LiveData<Event<Unit>> = _goBack
|
||||
|
||||
private val _error = MutableLiveData<Event<Int>>()
|
||||
val error: LiveData<Event<Int>> = _error
|
||||
|
||||
val secretKey = MutableLiveData<String>("")
|
||||
|
||||
val accountName = MutableLiveData<String>("")
|
||||
|
||||
fun loadOldAccount(accountId: String?) {
|
||||
accountId ?: return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
appDatabase.getDao().getAccount(accountId)?.let { accountModel ->
|
||||
accountName.postValue(accountModel.title ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAccount(accountId: String?) {
|
||||
viewModelScope.launch {
|
||||
val secretKey = secretKey.value
|
||||
val accountName = accountName.value
|
||||
|
||||
if (accountId == null) {
|
||||
if (secretKey.isNullOrEmpty()) {
|
||||
_error.postValue(Event(R.string.alert_black_secret_key))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
if (accountName.isNullOrEmpty()) {
|
||||
_error.postValue(Event(R.string.alert_black_account_name))
|
||||
return@launch
|
||||
}
|
||||
|
||||
val accountModel = if (accountId == null) {
|
||||
AccountModel(password = secretKey, title = accountName, type = AccountType.TOTP)
|
||||
} else {
|
||||
appDatabase.getDao().getAccount(accountId)?.also {
|
||||
it.title = accountName
|
||||
}
|
||||
}
|
||||
|
||||
accountModel?.let { appDatabase.getDao().insertOrUpdateAccount(it) }
|
||||
_goBack.postValue(Event(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
fun setSecretKey(secretKey: String) {
|
||||
this.secretKey.value = secretKey
|
||||
}
|
||||
|
||||
fun setAccountName(accountName: String) {
|
||||
this.accountName.value = accountName
|
||||
}
|
||||
|
||||
fun deleteAccount(accountId: String, onDeleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
appDatabase.getDao().deleteAccount(accountId)
|
||||
onDeleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
package com.yogeshpaliyal.keypass.ui.addTOTP
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.yogeshpaliyal.common.constants.RequestCodes
|
||||
import com.yogeshpaliyal.keypass.databinding.ActivityScannerBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScannerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityScannerBinding
|
||||
|
||||
private val REQUEST_CAM_PERMISSION = 432
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun start(activity: Activity) {
|
||||
val starter = Intent(activity, ScannerActivity::class.java)
|
||||
activity.startActivityForResult(starter, RequestCodes.SCANNER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityScannerBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == REQUEST_CAM_PERMISSION) {
|
||||
if (isAllRequestGranted(grantResults)) {
|
||||
// codeScanner?.startPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAllRequestGranted(grantResults: IntArray) =
|
||||
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.addTOTP
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.yogeshpaliyal.common.constants.RequestCodes
|
||||
import com.yogeshpaliyal.keypass.databinding.ActivityScannerBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScannerActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityScannerBinding
|
||||
|
||||
private val REQUEST_CAM_PERMISSION = 432
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun start(activity: Activity) {
|
||||
val starter = Intent(activity, ScannerActivity::class.java)
|
||||
activity.startActivityForResult(starter, RequestCodes.SCANNER)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityScannerBinding.inflate(layoutInflater)
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.toolbar)
|
||||
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == REQUEST_CAM_PERMISSION) {
|
||||
if (isAllRequestGranted(grantResults)) {
|
||||
// codeScanner?.startPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAllRequestGranted(grantResults: IntArray) =
|
||||
grantResults.all { it == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
||||
@@ -1,90 +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)
|
||||
}
|
||||
}
|
||||
}
|
||||
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.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.AuthState
|
||||
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,77 +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))
|
||||
}
|
||||
}
|
||||
}
|
||||
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.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.AuthState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
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,330 +1,251 @@
|
||||
package com.yogeshpaliyal.keypass.ui.backup
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceCategory
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
|
||||
import com.yogeshpaliyal.common.utils.backupAccounts
|
||||
import com.yogeshpaliyal.common.utils.canUserAccessBackupDirectory
|
||||
import com.yogeshpaliyal.common.utils.clearBackupKey
|
||||
import com.yogeshpaliyal.common.utils.formatCalendar
|
||||
import com.yogeshpaliyal.common.utils.getBackupDirectory
|
||||
import com.yogeshpaliyal.common.utils.getBackupTime
|
||||
import com.yogeshpaliyal.common.utils.getOrCreateBackupKey
|
||||
import com.yogeshpaliyal.common.utils.isAutoBackupEnabled
|
||||
import com.yogeshpaliyal.common.utils.isKeyPresent
|
||||
import com.yogeshpaliyal.common.utils.overrideAutoBackup
|
||||
import com.yogeshpaliyal.common.utils.saveKeyphrase
|
||||
import com.yogeshpaliyal.common.utils.setAutoBackupEnabled
|
||||
import com.yogeshpaliyal.common.utils.setBackupDirectory
|
||||
import com.yogeshpaliyal.common.utils.setBackupTime
|
||||
import com.yogeshpaliyal.common.utils.setOverrideAutoBackup
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.databinding.BackupActivityBinding
|
||||
import com.yogeshpaliyal.keypass.databinding.LayoutBackupKeypharseBinding
|
||||
import com.yogeshpaliyal.keypass.databinding.LayoutCustomKeypharseBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLDecoder
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class BackupActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun start(context: Context?) {
|
||||
val starter = Intent(context, BackupActivity::class.java)
|
||||
context?.startActivity(starter)
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var binding: BackupActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = BackupActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setSupportActionBar(binding.toolbar)
|
||||
binding.toolbar.setNavigationOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, SettingsFragment())
|
||||
.commit()
|
||||
}
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
@AndroidEntryPoint
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
|
||||
private val CHOOSE_BACKUPS_LOCATION_REQUEST_CODE = 26212
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.backup_preferences, rootKey)
|
||||
updateItems()
|
||||
}
|
||||
|
||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
||||
when (preference.key) {
|
||||
getString(R.string.settings_start_backup) -> {
|
||||
startBackup()
|
||||
}
|
||||
getString(R.string.settings_create_backup) -> {
|
||||
lifecycleScope.launch {
|
||||
if (context.canUserAccessBackupDirectory()) {
|
||||
val selectedDirectory = Uri.parse(context.getBackupDirectory())
|
||||
passwordSelection(selectedDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
getString(R.string.settings_backup_folder) -> {
|
||||
changeBackupFolder()
|
||||
}
|
||||
getString(R.string.settings_verify_key_phrase) -> {
|
||||
verifyKeyPhrase()
|
||||
}
|
||||
getString(R.string.settings_stop_backup) -> {
|
||||
lifecycleScope.launch {
|
||||
stopBackup()
|
||||
}
|
||||
}
|
||||
getString(R.string.settings_auto_backup) -> {
|
||||
lifecycleScope.launch {
|
||||
context.setAutoBackupEnabled(context.isAutoBackupEnabled().not())
|
||||
updateItems()
|
||||
}
|
||||
}
|
||||
getString(R.string.settings_override_auto_backup) -> {
|
||||
lifecycleScope.launch {
|
||||
context.setOverrideAutoBackup(context.overrideAutoBackup().not())
|
||||
updateItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
private suspend fun passwordSelection(selectedDirectory: Uri) {
|
||||
val isKeyPresent = context?.isKeyPresent() ?: return
|
||||
if (isKeyPresent) {
|
||||
backup(selectedDirectory)
|
||||
return
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.alert)
|
||||
.setMessage(getString(R.string.custom_generated_keyphrase_info))
|
||||
.setPositiveButton(
|
||||
getString(R.string.custom_keyphrase)
|
||||
) { dialog, which ->
|
||||
dialog?.dismiss()
|
||||
setCustomKeyphrase(selectedDirectory)
|
||||
}
|
||||
.setNegativeButton(R.string.generate_keyphrase) { dialog, which ->
|
||||
dialog?.dismiss()
|
||||
backup(selectedDirectory)
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun setCustomKeyphrase(selectedDirectory: Uri) {
|
||||
val binding = LayoutCustomKeypharseBinding.inflate(layoutInflater)
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
|
||||
.setPositiveButton(
|
||||
getString(R.string.yes)
|
||||
) { dialog, which ->
|
||||
|
||||
dialog?.dismiss()
|
||||
}.create()
|
||||
dialog.setOnShowListener {
|
||||
val positiveBtn = dialog.getButton(DialogInterface.BUTTON_POSITIVE)
|
||||
positiveBtn.setOnClickListener {
|
||||
val keyphrase = binding.etKeyPhrase.text.toString().trim()
|
||||
if (keyphrase.isEmpty()) {
|
||||
Toast.makeText(context, R.string.alert_blank_keyphrase, Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
if (keyphrase.length != BACKUP_KEY_LENGTH) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
R.string.alert_invalid_keyphrase,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
context?.saveKeyphrase(keyphrase)
|
||||
backup(selectedDirectory)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
fun backup(selectedDirectory: Uri) {
|
||||
lifecycleScope.launch {
|
||||
context.backupAccounts(appDb, selectedDirectory)?.let { keyPair ->
|
||||
if (keyPair.first) {
|
||||
val binding = LayoutBackupKeypharseBinding.inflate(layoutInflater)
|
||||
binding.txtCode.text = context?.getOrCreateBackupKey()?.second ?: ""
|
||||
binding.txtCode.setOnClickListener {
|
||||
val clipboard =
|
||||
ContextCompat.getSystemService(
|
||||
requireContext(),
|
||||
ClipboardManager::class.java
|
||||
)
|
||||
val clip = ClipData.newPlainText("KeyPass", 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(
|
||||
getString(R.string.yes)
|
||||
) { dialog, which ->
|
||||
updateItems()
|
||||
dialog?.dismiss()
|
||||
}.show()
|
||||
} else {
|
||||
updateItems()
|
||||
Toast.makeText(
|
||||
context,
|
||||
getString(R.string.backup_completed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateItems() {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val isBackupEnabled =
|
||||
context.canUserAccessBackupDirectory() && (context?.isKeyPresent() ?: false)
|
||||
|
||||
val isAutoBackupEnabled = context.isAutoBackupEnabled()
|
||||
val overrideAutoBackup = context.overrideAutoBackup()
|
||||
|
||||
val lastBackupTime = context.getBackupTime()
|
||||
val backupDirectory = context.getBackupDirectory()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
findPreference<Preference>(getString(R.string.settings_start_backup))?.isVisible =
|
||||
isBackupEnabled.not()
|
||||
findPreference<Preference>(getString(R.string.settings_stop_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_auto_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
findPreference<Preference>(getString(R.string.settings_auto_backup))?.summary =
|
||||
if (isAutoBackupEnabled) getString(R.string.enabled) else getString(R.string.disabled)
|
||||
|
||||
findPreference<PreferenceCategory>(getString(R.string.settings_cat_auto_backup))?.isVisible =
|
||||
isBackupEnabled && isAutoBackupEnabled
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_override_auto_backup))?.summary =
|
||||
if (overrideAutoBackup) getString(R.string.enabled) else getString(R.string.disabled)
|
||||
|
||||
findPreference<Preference>(getString(R.string.settings_create_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
findPreference<Preference>(getString(R.string.settings_create_backup))?.summary =
|
||||
getString(
|
||||
R.string.last_backup_date,
|
||||
lastBackupTime.formatCalendar("dd MMM yyyy hh:mm aa")
|
||||
)
|
||||
findPreference<Preference>(getString(R.string.settings_backup_folder))?.isVisible =
|
||||
isBackupEnabled
|
||||
val directory = URLDecoder.decode(backupDirectory, "utf-8").split("/")
|
||||
val folderName = directory.get(directory.lastIndex)
|
||||
findPreference<Preference>(getString(R.string.settings_backup_folder))?.summary =
|
||||
folderName
|
||||
findPreference<Preference>(getString(R.string.settings_verify_key_phrase))?.isVisible =
|
||||
false
|
||||
findPreference<Preference>(getString(R.string.settings_backup))?.isVisible =
|
||||
isBackupEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startBackup() {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
|
||||
intent.addFlags(
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, CHOOSE_BACKUPS_LOCATION_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())
|
||||
passwordSelection(selectedDirectory)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeBackupFolder() {
|
||||
startBackup()
|
||||
}
|
||||
|
||||
private fun verifyKeyPhrase() {
|
||||
Toast.makeText(context, getString(R.string.coming_soon), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private suspend fun stopBackup() {
|
||||
context.clearBackupKey()
|
||||
context.setBackupDirectory("")
|
||||
context.setBackupTime(-1)
|
||||
context.setOverrideAutoBackup(false)
|
||||
context.setAutoBackupEnabled(false)
|
||||
updateItems()
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.backup
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yogeshpaliyal.common.utils.canUserAccessBackupDirectory
|
||||
import com.yogeshpaliyal.common.utils.clearBackupKey
|
||||
import com.yogeshpaliyal.common.utils.formatCalendar
|
||||
import com.yogeshpaliyal.common.utils.getBackupDirectory
|
||||
import com.yogeshpaliyal.common.utils.getBackupTime
|
||||
import com.yogeshpaliyal.common.utils.isAutoBackupEnabled
|
||||
import com.yogeshpaliyal.common.utils.isKeyPresent
|
||||
import com.yogeshpaliyal.common.utils.overrideAutoBackup
|
||||
import com.yogeshpaliyal.common.utils.setAutoBackupEnabled
|
||||
import com.yogeshpaliyal.common.utils.setBackupDirectory
|
||||
import com.yogeshpaliyal.common.utils.setOverrideAutoBackup
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.backup.components.BackupDialogs
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.SelectKeyphraseType
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.ShowKeyphrase
|
||||
import com.yogeshpaliyal.keypass.ui.settings.PreferenceItem
|
||||
import kotlinx.coroutines.launch
|
||||
import org.reduxkotlin.compose.rememberTypedDispatcher
|
||||
|
||||
@Composable
|
||||
fun BackupScreen(state: BackupScreenState) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val dispatchAction = rememberTypedDispatcher<Action>()
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(KeyPassBackupDirectoryPick()) {
|
||||
if (it == null) {
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
|
||||
context.contentResolver.takePersistableUriPermission(
|
||||
it,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
)
|
||||
|
||||
coroutineScope.launch {
|
||||
val dialog = if (context.isKeyPresent()) {
|
||||
ShowKeyphrase
|
||||
} else {
|
||||
SelectKeyphraseType
|
||||
}
|
||||
dispatchAction(
|
||||
StateUpdateAction(
|
||||
state.copy(
|
||||
backupDirectory = it,
|
||||
dialog = dialog
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = state, block = {
|
||||
state.backupDirectory?.let {
|
||||
context.setBackupDirectory(it.toString())
|
||||
}
|
||||
state.isAutoBackupEnabled?.let {
|
||||
context.setAutoBackupEnabled(it)
|
||||
}
|
||||
state.overrideAutoBackup?.let {
|
||||
context.setOverrideAutoBackup(it)
|
||||
}
|
||||
})
|
||||
|
||||
LaunchedEffect(key1 = state.isBackupEnabled, block = {
|
||||
state.isBackupEnabled?.let {
|
||||
if (it.not()) {
|
||||
context.clearBackupKey()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
LaunchedEffect(key1 = Unit, block = {
|
||||
val isBackupEnabled = (
|
||||
context.canUserAccessBackupDirectory() && (context.isKeyPresent())
|
||||
)
|
||||
|
||||
val isAutoBackupEnabled = context.isAutoBackupEnabled()
|
||||
val overrideAutoBackup = context.overrideAutoBackup()
|
||||
|
||||
val lastBackupTime = context.getBackupTime()
|
||||
val backupDirectory = context.getBackupDirectory()
|
||||
|
||||
dispatchAction(
|
||||
StateUpdateAction(
|
||||
state.copy(
|
||||
isBackupEnabled = isBackupEnabled,
|
||||
isAutoBackupEnabled = isAutoBackupEnabled,
|
||||
overrideAutoBackup = overrideAutoBackup,
|
||||
lastBackupTime = lastBackupTime,
|
||||
backupDirectory = Uri.parse(backupDirectory)
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
Scaffold(bottomBar = {
|
||||
BottomAppBar {
|
||||
IconButton(onClick = {
|
||||
dispatchAction(GoBackAction)
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
|
||||
contentDescription = "Go Back",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}) { contentPadding ->
|
||||
Surface(modifier = Modifier.padding(contentPadding)) {
|
||||
BackSettingOptions(state, updatedState = {
|
||||
dispatchAction(StateUpdateAction(it))
|
||||
}) {
|
||||
launcher.launch(arrayOf())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BackupDialogs(state = state) {
|
||||
dispatchAction(StateUpdateAction(it))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackSettingOptions(
|
||||
state: BackupScreenState,
|
||||
updatedState: (BackupScreenState) -> Unit,
|
||||
launchDirectorySelector: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
PreferenceItem(summary = R.string.backup_desc)
|
||||
AnimatedVisibility(visible = state.isBackupEnabled == true) {
|
||||
BackupEnableOptions(state, updatedState, launchDirectorySelector)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = state.isBackupEnabled != true) {
|
||||
PreferenceItem(title = R.string.turn_on_backup, onClickItem = launchDirectorySelector)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BackupEnableOptions(
|
||||
state: BackupScreenState,
|
||||
updatedState: (BackupScreenState) -> Unit,
|
||||
launchDirectorySelector: () -> Unit
|
||||
) {
|
||||
Column {
|
||||
PreferenceItem(
|
||||
title = R.string.create_backup,
|
||||
summaryStr = stringResource(
|
||||
id = R.string.last_backup_date,
|
||||
state.lastBackupTime?.formatCalendar("dd MMM yyyy hh:mm aa") ?: ""
|
||||
)
|
||||
) {
|
||||
updatedState(state.copy(dialog = ShowKeyphrase))
|
||||
}
|
||||
PreferenceItem(
|
||||
title = R.string.backup_folder,
|
||||
summaryStr = state.getFormattedBackupDirectory(),
|
||||
onClickItem = launchDirectorySelector
|
||||
)
|
||||
|
||||
AutoBackup(state.isAutoBackupEnabled, state.overrideAutoBackup, {
|
||||
updatedState(state.copy(isAutoBackupEnabled = it))
|
||||
}) {
|
||||
updatedState(state.copy(overrideAutoBackup = it))
|
||||
}
|
||||
|
||||
// PreferenceItem(
|
||||
// title = R.string.verify_keyphrase,
|
||||
// summary = R.string.verify_keyphrase_message
|
||||
// )
|
||||
PreferenceItem(title = R.string.turn_off_backup) {
|
||||
updatedState(
|
||||
state.copy(
|
||||
isBackupEnabled = false,
|
||||
isAutoBackupEnabled = false,
|
||||
overrideAutoBackup = false,
|
||||
lastBackupTime = -1,
|
||||
backupDirectory = null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoBackup(
|
||||
isAutoBackupEnabled: Boolean?,
|
||||
overrideAutoBackup: Boolean?,
|
||||
setAutoBackupEnabled: (Boolean) -> Unit,
|
||||
setOverrideAutoBackup: (Boolean) -> Unit
|
||||
) {
|
||||
PreferenceItem(
|
||||
title = R.string.auto_backup,
|
||||
summary = if (isAutoBackupEnabled == true) R.string.enabled else R.string.disabled
|
||||
) {
|
||||
setAutoBackupEnabled(!(isAutoBackupEnabled ?: false))
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = isAutoBackupEnabled == true) {
|
||||
Column {
|
||||
PreferenceItem(title = R.string.auto_backup, isCategory = true)
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(1.dp)
|
||||
)
|
||||
PreferenceItem(summary = R.string.auto_backup_desc)
|
||||
PreferenceItem(
|
||||
title = R.string.override_auto_backup_file,
|
||||
summary = if (overrideAutoBackup == true) R.string.enabled else R.string.disabled
|
||||
) {
|
||||
setOverrideAutoBackup(!(overrideAutoBackup ?: false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.yogeshpaliyal.keypass.ui.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
||||
class KeyPassBackupDirectoryPick : ActivityResultContracts.OpenDocument() {
|
||||
override fun createIntent(context: Context, input: Array<String>): Intent {
|
||||
super.createIntent(context, input)
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
|
||||
intent.addFlags(
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.yogeshpaliyal.keypass.ui.backup.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.CustomKeyphrase
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.SelectKeyphraseType
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.ShowKeyphrase
|
||||
|
||||
@Composable
|
||||
fun BackupDialogs(state: BackupScreenState, updateState: (BackupScreenState) -> Unit) {
|
||||
when (state.dialog) {
|
||||
is SelectKeyphraseType -> {
|
||||
SelectKeyphraseType(customKeyphrase = {
|
||||
updateState(state.copy(dialog = CustomKeyphrase))
|
||||
}, generateKeyphrase = {
|
||||
updateState(state.copy(dialog = ShowKeyphrase))
|
||||
}) {
|
||||
updateState(state.copy(dialog = null))
|
||||
}
|
||||
}
|
||||
|
||||
is CustomKeyphrase -> {
|
||||
CreateCustomKeyphrase(saveKeyphrase = {
|
||||
updateState(state.copy(dialog = ShowKeyphrase))
|
||||
}) {
|
||||
updateState(state.copy(dialog = null))
|
||||
}
|
||||
}
|
||||
|
||||
is ShowKeyphrase -> {
|
||||
ShowKeyPhraseDialog(selectedDirectory = state.backupDirectory, onYesClicked = {
|
||||
updateState(state.copy(dialog = null, isBackupEnabled = true))
|
||||
}) {
|
||||
updateState(
|
||||
state.copy(
|
||||
isBackupEnabled = true,
|
||||
lastBackupTime = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
null -> {
|
||||
// Do Noting Do not show dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.yogeshpaliyal.keypass.ui.backup.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.AlertDialog
|
||||
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.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
|
||||
import com.yogeshpaliyal.common.utils.saveKeyphrase
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
|
||||
import kotlinx.coroutines.launch
|
||||
import org.reduxkotlin.compose.rememberTypedDispatcher
|
||||
|
||||
@Composable
|
||||
fun CreateCustomKeyphrase(saveKeyphrase: () -> Unit, dismissDialog: () -> Unit) {
|
||||
val (keyphrase, setKeyPhrase) = remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
val dispatchAction = rememberTypedDispatcher<Action>()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
AlertDialog(onDismissRequest = dismissDialog, title = {
|
||||
Text(text = stringResource(id = R.string.set_keyphrase))
|
||||
}, 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 {
|
||||
context.saveKeyphrase(keyphrase)
|
||||
}
|
||||
|
||||
saveKeyphrase.invoke()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.yes))
|
||||
}
|
||||
}, text = {
|
||||
Column {
|
||||
Text(text = stringResource(id = R.string.set_keyphrase_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))
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.yogeshpaliyal.keypass.ui.backup.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
|
||||
@Composable
|
||||
fun SelectKeyphraseType(
|
||||
customKeyphrase: () -> Unit,
|
||||
generateKeyphrase: () -> Unit,
|
||||
dismissDialog: () -> Unit
|
||||
) {
|
||||
AlertDialog(onDismissRequest = dismissDialog, title = {
|
||||
Text(text = stringResource(id = R.string.alert))
|
||||
}, confirmButton = {
|
||||
TextButton(onClick = customKeyphrase) {
|
||||
Text(stringResource(id = R.string.custom_keyphrase))
|
||||
}
|
||||
}, dismissButton = {
|
||||
TextButton(onClick = generateKeyphrase) {
|
||||
Text(stringResource(id = R.string.generate_keyphrase))
|
||||
}
|
||||
}, text = {
|
||||
Column {
|
||||
Text(text = stringResource(id = R.string.custom_generated_keyphrase_info))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.yogeshpaliyal.keypass.ui.backup.components
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.yogeshpaliyal.common.utils.backupAccounts
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
|
||||
import com.yogeshpaliyal.keypass.ui.settings.MySettingsViewModel
|
||||
import org.reduxkotlin.compose.rememberTypedDispatcher
|
||||
|
||||
@Composable
|
||||
fun ShowKeyPhraseDialog(
|
||||
selectedDirectory: Uri?,
|
||||
mySettingsViewModel: MySettingsViewModel = hiltViewModel(),
|
||||
onYesClicked: () -> Unit,
|
||||
saveKeyphrase: () -> Unit
|
||||
) {
|
||||
if (selectedDirectory == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val dispatchAction = rememberTypedDispatcher<Action>()
|
||||
|
||||
val context = LocalContext.current
|
||||
val (backupInfo, setBackupInfo) = remember {
|
||||
mutableStateOf<Pair<Boolean, String>?>(null)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = selectedDirectory, block = {
|
||||
val localBackupInfo = context.backupAccounts(
|
||||
mySettingsViewModel.appDb,
|
||||
selectedDirectory
|
||||
)
|
||||
setBackupInfo(localBackupInfo)
|
||||
saveKeyphrase()
|
||||
})
|
||||
|
||||
if (backupInfo != null) {
|
||||
val newKeyCreated = backupInfo.first
|
||||
if (newKeyCreated) {
|
||||
AlertDialog(onDismissRequest = {}, title = {
|
||||
Text(text = stringResource(id = R.string.alert))
|
||||
}, confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onYesClicked()
|
||||
}) {
|
||||
Text(stringResource(id = R.string.yes))
|
||||
}
|
||||
}, text = {
|
||||
Column {
|
||||
Text(text = stringResource(id = R.string.copy_keypharse_msg))
|
||||
TextButton(onClick = {
|
||||
dispatchAction(CopyToClipboard(backupInfo.second))
|
||||
}) {
|
||||
Text(text = backupInfo.second)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dispatchAction(ToastAction(R.string.backup_completed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,356 +1,356 @@
|
||||
package com.yogeshpaliyal.keypass.ui.detail
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
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.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material.icons.rounded.Visibility
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.common.utils.PasswordGenerator
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.GoBackAction
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* yogeshpaliyal.foss@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 10:38
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun AccountDetailPage(
|
||||
id: Long?,
|
||||
viewModel: DetailViewModel = hiltViewModel()
|
||||
) {
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
// task value state
|
||||
val (accountModel, setAccountModel) = remember {
|
||||
mutableStateOf(
|
||||
AccountModel()
|
||||
)
|
||||
}
|
||||
|
||||
// Set initial object
|
||||
LaunchedEffect(key1 = id) {
|
||||
viewModel.loadAccount(id) {
|
||||
setAccountModel(it.copy())
|
||||
}
|
||||
}
|
||||
|
||||
val goBack: () -> Unit = {
|
||||
dispatchAction(GoBackAction)
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(QRScanner()) {
|
||||
it?.let {
|
||||
setAccountModel(accountModel.copy(password = it))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomBar(
|
||||
accountModel,
|
||||
backPressed = goBack,
|
||||
onDeleteAccount = {
|
||||
viewModel.deleteAccount(accountModel, goBack)
|
||||
}
|
||||
) {
|
||||
viewModel.insertOrUpdate(accountModel, goBack)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Surface(modifier = Modifier.padding(paddingValues)) {
|
||||
Fields(
|
||||
accountModel = accountModel,
|
||||
updateAccountModel = { newAccountModel ->
|
||||
setAccountModel(newAccountModel)
|
||||
}
|
||||
) {
|
||||
launcher.launch(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Fields(
|
||||
modifier: Modifier = Modifier,
|
||||
accountModel: AccountModel,
|
||||
updateAccountModel: (newAccountModel: AccountModel) -> Unit,
|
||||
scanClicked: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("accountName"),
|
||||
placeholder = R.string.account_name,
|
||||
value = accountModel.title,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(title = it))
|
||||
}
|
||||
)
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("username"),
|
||||
placeholder = R.string.username_email_phone,
|
||||
value = accountModel.username,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(username = it))
|
||||
}
|
||||
)
|
||||
|
||||
Column {
|
||||
val passwordVisible = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val visualTransformation =
|
||||
if (passwordVisible.value) VisualTransformation.None else PasswordVisualTransformation()
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("password"),
|
||||
placeholder = R.string.password,
|
||||
value = accountModel.password,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(password = it))
|
||||
},
|
||||
trailingIcon = {
|
||||
PasswordTrailingIcon(passwordVisible.value) {
|
||||
passwordVisible.value = it
|
||||
}
|
||||
},
|
||||
leadingIcon = if (accountModel.id != null) {
|
||||
null
|
||||
} else (
|
||||
{
|
||||
IconButton(
|
||||
onClick = {
|
||||
updateAccountModel(accountModel.copy(password = PasswordGenerator().generatePassword()))
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Refresh),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
visualTransformation = visualTransformation
|
||||
)
|
||||
Button(onClick = scanClicked) {
|
||||
Row {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_twotone_qr_code_scanner_24),
|
||||
contentDescription = ""
|
||||
)
|
||||
Text(text = stringResource(id = R.string.scan_password))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("tags"),
|
||||
placeholder = R.string.tags_comma_separated_optional,
|
||||
value = accountModel.tags,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(tags = it))
|
||||
}
|
||||
)
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("website"),
|
||||
placeholder = R.string.website_url_optional,
|
||||
value = accountModel.site,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(site = it))
|
||||
}
|
||||
)
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("notes"),
|
||||
placeholder = R.string.notes_optional,
|
||||
value = accountModel.notes,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(notes = it))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteConfirmation(
|
||||
openDialog: Boolean,
|
||||
updateDialogVisibility: (Boolean) -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
if (openDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { /*TODO*/ },
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.delete_account_title))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
modifier = Modifier.testTag("delete"),
|
||||
onClick = {
|
||||
updateDialogVisibility(false)
|
||||
onDelete()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.delete))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.delete_account_msg))
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
updateDialogVisibility(false)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyPassInputField(
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes placeholder: Int,
|
||||
value: String?,
|
||||
setValue: (String) -> Unit,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
value = value ?: "",
|
||||
label = {
|
||||
Text(text = stringResource(id = placeholder))
|
||||
},
|
||||
onValueChange = setValue,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
visualTransformation = visualTransformation
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PasswordTrailingIcon(
|
||||
passwordVisible: Boolean,
|
||||
changePasswordVisibility: (updatedValue: Boolean) -> Unit
|
||||
) {
|
||||
val description = if (passwordVisible) "Hide password" else "Show password"
|
||||
|
||||
val image = if (passwordVisible) {
|
||||
Icons.Rounded.Visibility
|
||||
} else {
|
||||
Icons.Rounded.VisibilityOff
|
||||
}
|
||||
|
||||
IconButton(onClick = { changePasswordVisibility(!passwordVisible) }) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = image),
|
||||
contentDescription = description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomBar(
|
||||
accountModel: AccountModel,
|
||||
backPressed: () -> Unit,
|
||||
onDeleteAccount: () -> Unit,
|
||||
onSaveClicked: () -> Unit
|
||||
) {
|
||||
val openDialog = remember { mutableStateOf(false) }
|
||||
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = backPressed) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
|
||||
contentDescription = "Go Back",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
if (accountModel.id != null) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag("action_delete"),
|
||||
onClick = { openDialog.value = true }
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Delete),
|
||||
contentDescription = "Delete",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Done),
|
||||
contentDescription = "Save Changes"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DeleteConfirmation(
|
||||
openDialog.value,
|
||||
updateDialogVisibility = {
|
||||
openDialog.value = it
|
||||
},
|
||||
onDeleteAccount
|
||||
)
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.detail
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.annotation.StringRes
|
||||
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.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.Done
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material.icons.rounded.Visibility
|
||||
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.common.utils.PasswordGenerator
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* yogeshpaliyal.foss@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 10:38
|
||||
*/
|
||||
|
||||
@Composable
|
||||
fun AccountDetailPage(
|
||||
id: Long?,
|
||||
viewModel: DetailViewModel = hiltViewModel()
|
||||
) {
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
// task value state
|
||||
val (accountModel, setAccountModel) = remember {
|
||||
mutableStateOf(
|
||||
AccountModel()
|
||||
)
|
||||
}
|
||||
|
||||
// Set initial object
|
||||
LaunchedEffect(key1 = id) {
|
||||
viewModel.loadAccount(id) {
|
||||
setAccountModel(it.copy())
|
||||
}
|
||||
}
|
||||
|
||||
val goBack: () -> Unit = {
|
||||
dispatchAction(GoBackAction)
|
||||
}
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(QRScanner()) {
|
||||
it?.let {
|
||||
setAccountModel(accountModel.copy(password = it))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
BottomBar(
|
||||
accountModel,
|
||||
backPressed = goBack,
|
||||
onDeleteAccount = {
|
||||
viewModel.deleteAccount(accountModel, goBack)
|
||||
}
|
||||
) {
|
||||
viewModel.insertOrUpdate(accountModel, goBack)
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Surface(modifier = Modifier.padding(paddingValues)) {
|
||||
Fields(
|
||||
accountModel = accountModel,
|
||||
updateAccountModel = { newAccountModel ->
|
||||
setAccountModel(newAccountModel)
|
||||
}
|
||||
) {
|
||||
launcher.launch(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Fields(
|
||||
modifier: Modifier = Modifier,
|
||||
accountModel: AccountModel,
|
||||
updateAccountModel: (newAccountModel: AccountModel) -> Unit,
|
||||
scanClicked: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("accountName"),
|
||||
placeholder = R.string.account_name,
|
||||
value = accountModel.title,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(title = it))
|
||||
}
|
||||
)
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("username"),
|
||||
placeholder = R.string.username_email_phone,
|
||||
value = accountModel.username,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(username = it))
|
||||
}
|
||||
)
|
||||
|
||||
Column {
|
||||
val passwordVisible = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
val visualTransformation =
|
||||
if (passwordVisible.value) VisualTransformation.None else PasswordVisualTransformation()
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("password"),
|
||||
placeholder = R.string.password,
|
||||
value = accountModel.password,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(password = it))
|
||||
},
|
||||
trailingIcon = {
|
||||
PasswordTrailingIcon(passwordVisible.value) {
|
||||
passwordVisible.value = it
|
||||
}
|
||||
},
|
||||
leadingIcon = if (accountModel.id != null) {
|
||||
null
|
||||
} else (
|
||||
{
|
||||
IconButton(
|
||||
onClick = {
|
||||
updateAccountModel(accountModel.copy(password = PasswordGenerator().generatePassword()))
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Refresh),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
),
|
||||
visualTransformation = visualTransformation
|
||||
)
|
||||
Button(onClick = scanClicked) {
|
||||
Row {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_twotone_qr_code_scanner_24),
|
||||
contentDescription = ""
|
||||
)
|
||||
Text(text = stringResource(id = R.string.scan_password))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("tags"),
|
||||
placeholder = R.string.tags_comma_separated_optional,
|
||||
value = accountModel.tags,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(tags = it))
|
||||
}
|
||||
)
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("website"),
|
||||
placeholder = R.string.website_url_optional,
|
||||
value = accountModel.site,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(site = it))
|
||||
}
|
||||
)
|
||||
|
||||
KeyPassInputField(
|
||||
modifier = Modifier.testTag("notes"),
|
||||
placeholder = R.string.notes_optional,
|
||||
value = accountModel.notes,
|
||||
setValue = {
|
||||
updateAccountModel(accountModel.copy(notes = it))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DeleteConfirmation(
|
||||
openDialog: Boolean,
|
||||
updateDialogVisibility: (Boolean) -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
if (openDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { /*TODO*/ },
|
||||
title = {
|
||||
Text(text = stringResource(id = R.string.delete_account_title))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
modifier = Modifier.testTag("delete"),
|
||||
onClick = {
|
||||
updateDialogVisibility(false)
|
||||
onDelete()
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.delete))
|
||||
}
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(id = R.string.delete_account_msg))
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
updateDialogVisibility(false)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(id = R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyPassInputField(
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes placeholder: Int,
|
||||
value: String?,
|
||||
setValue: (String) -> Unit,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
trailingIcon: @Composable (() -> Unit)? = null,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
value = value ?: "",
|
||||
label = {
|
||||
Text(text = stringResource(id = placeholder))
|
||||
},
|
||||
onValueChange = setValue,
|
||||
leadingIcon = leadingIcon,
|
||||
trailingIcon = trailingIcon,
|
||||
visualTransformation = visualTransformation
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PasswordTrailingIcon(
|
||||
passwordVisible: Boolean,
|
||||
changePasswordVisibility: (updatedValue: Boolean) -> Unit
|
||||
) {
|
||||
val description = if (passwordVisible) "Hide password" else "Show password"
|
||||
|
||||
val image = if (passwordVisible) {
|
||||
Icons.Rounded.Visibility
|
||||
} else {
|
||||
Icons.Rounded.VisibilityOff
|
||||
}
|
||||
|
||||
IconButton(onClick = { changePasswordVisibility(!passwordVisible) }) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = image),
|
||||
contentDescription = description
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomBar(
|
||||
accountModel: AccountModel,
|
||||
backPressed: () -> Unit,
|
||||
onDeleteAccount: () -> Unit,
|
||||
onSaveClicked: () -> Unit
|
||||
) {
|
||||
val openDialog = remember { mutableStateOf(false) }
|
||||
|
||||
BottomAppBar(
|
||||
actions = {
|
||||
IconButton(onClick = backPressed) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.ArrowBackIosNew),
|
||||
contentDescription = "Go Back",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
if (accountModel.id != null) {
|
||||
IconButton(
|
||||
modifier = Modifier.testTag("action_delete"),
|
||||
onClick = { openDialog.value = true }
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Delete),
|
||||
contentDescription = "Delete",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(modifier = Modifier.testTag("save"), onClick = onSaveClicked) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Done),
|
||||
contentDescription = "Save Changes"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
DeleteConfirmation(
|
||||
openDialog.value,
|
||||
updateDialogVisibility = {
|
||||
openDialog.value = it
|
||||
},
|
||||
onDeleteAccount
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,65 +1,66 @@
|
||||
package com.yogeshpaliyal.keypass.ui.detail
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 11:52
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DetailViewModel @Inject constructor(
|
||||
app: Application,
|
||||
val appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
private val _accountModel by lazy { MutableLiveData<AccountModel>() }
|
||||
val accountModel: LiveData<AccountModel> = _accountModel
|
||||
|
||||
fun loadAccount(accountId: Long?, getAccount: (AccountModel) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
getAccount(appDb.getDao().getAccount(accountId) ?: AccountModel())
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAccount(accountModel: AccountModel, onExecCompleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
accountModel.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
appDb.getDao().deleteAccount(it)
|
||||
}
|
||||
autoBackup()
|
||||
onExecCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertOrUpdate(accountModel: AccountModel, onExecCompleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
accountModel.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
appDb.getDao().insertOrUpdateAccount(it)
|
||||
autoBackup()
|
||||
}
|
||||
}
|
||||
onExecCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun autoBackup() {
|
||||
viewModelScope.launch {
|
||||
// application.executeAutoBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.detail
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.common.worker.executeAutoBackup
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 11:52
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DetailViewModel @Inject constructor(
|
||||
val app: Application,
|
||||
val appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
) : AndroidViewModel(app) {
|
||||
|
||||
private val _accountModel by lazy { MutableLiveData<AccountModel>() }
|
||||
val accountModel: LiveData<AccountModel> = _accountModel
|
||||
|
||||
fun loadAccount(accountId: Long?, getAccount: (AccountModel) -> Unit) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
getAccount(appDb.getDao().getAccount(accountId) ?: AccountModel())
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteAccount(accountModel: AccountModel, onExecCompleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
accountModel.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
appDb.getDao().deleteAccount(it)
|
||||
}
|
||||
autoBackup()
|
||||
onExecCompleted()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun insertOrUpdate(accountModel: AccountModel, onExecCompleted: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
accountModel.let {
|
||||
withContext(Dispatchers.IO) {
|
||||
appDb.getDao().insertOrUpdateAccount(it)
|
||||
autoBackup()
|
||||
}
|
||||
}
|
||||
onExecCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
private fun autoBackup() {
|
||||
viewModelScope.launch {
|
||||
app.executeAutoBackup()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
package com.yogeshpaliyal.keypass.ui.detail
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
|
||||
class QRScanner : ActivityResultContract<Void?, String?>() {
|
||||
override fun createIntent(context: Context, input: Void?): Intent {
|
||||
val intentIntegration = if (context is Activity) {
|
||||
IntentIntegrator(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return intentIntegration?.setPrompt("")?.createScanIntent() ?: Intent()
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, result: Intent?): String? {
|
||||
return IntentIntegrator.parseActivityResult(
|
||||
IntentIntegrator.REQUEST_CODE,
|
||||
resultCode,
|
||||
result
|
||||
)?.contents
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.detail
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
import com.google.zxing.integration.android.IntentIntegrator
|
||||
|
||||
class QRScanner : ActivityResultContract<Void?, String?>() {
|
||||
override fun createIntent(context: Context, input: Void?): Intent {
|
||||
val intentIntegration = if (context is Activity) {
|
||||
IntentIntegrator(context)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return intentIntegration?.setPrompt("")?.createScanIntent() ?: Intent()
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, result: Intent?): String? {
|
||||
return IntentIntegrator.parseActivityResult(
|
||||
IntentIntegrator.REQUEST_CODE,
|
||||
resultCode,
|
||||
result
|
||||
)?.contents
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.yogeshpaliyal.keypass.ui.generate.ui.GeneratePasswordScreen
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GeneratePasswordActivity : AppCompatActivity() {
|
||||
private val viewModel: GeneratePasswordViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
Mdc3Theme {
|
||||
GeneratePasswordScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.yogeshpaliyal.keypass.ui.generate.ui.GeneratePasswordScreen
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
@AndroidEntryPoint
|
||||
class GeneratePasswordActivity : AppCompatActivity() {
|
||||
private val viewModel: GeneratePasswordViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContent {
|
||||
Mdc3Theme {
|
||||
GeneratePasswordScreen(viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,63 +1,63 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.yogeshpaliyal.common.utils.PasswordGenerator
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GeneratePasswordViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val _viewState = MutableStateFlow(GeneratePasswordViewState.Initial)
|
||||
val viewState = _viewState.asStateFlow()
|
||||
|
||||
fun generatePassword() {
|
||||
val currentViewState = _viewState.value
|
||||
|
||||
val passwordGenerator = PasswordGenerator(
|
||||
length = currentViewState.length,
|
||||
includeUpperCaseLetters = currentViewState.includeUppercaseLetters,
|
||||
includeLowerCaseLetters = currentViewState.includeLowercaseLetters,
|
||||
includeSymbols = currentViewState.includeSymbols,
|
||||
includeNumbers = currentViewState.includeNumbers
|
||||
)
|
||||
|
||||
_viewState.update {
|
||||
val newPassword = passwordGenerator.generatePassword()
|
||||
it.copy(password = newPassword)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPasswordLengthSliderChange(value: Float) {
|
||||
_viewState.update {
|
||||
it.copy(length = value.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
fun onUppercaseCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeUppercaseLetters = checked)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLowercaseCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeLowercaseLetters = checked)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNumbersCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeNumbers = checked)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSymbolsCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeSymbols = checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.yogeshpaliyal.common.utils.PasswordGenerator
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class GeneratePasswordViewModel @Inject constructor() : ViewModel() {
|
||||
|
||||
private val _viewState = MutableStateFlow(GeneratePasswordViewState.Initial)
|
||||
val viewState = _viewState.asStateFlow()
|
||||
|
||||
fun generatePassword() {
|
||||
val currentViewState = _viewState.value
|
||||
|
||||
val passwordGenerator = PasswordGenerator(
|
||||
length = currentViewState.length,
|
||||
includeUpperCaseLetters = currentViewState.includeUppercaseLetters,
|
||||
includeLowerCaseLetters = currentViewState.includeLowercaseLetters,
|
||||
includeSymbols = currentViewState.includeSymbols,
|
||||
includeNumbers = currentViewState.includeNumbers
|
||||
)
|
||||
|
||||
_viewState.update {
|
||||
val newPassword = passwordGenerator.generatePassword()
|
||||
it.copy(password = newPassword)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPasswordLengthSliderChange(value: Float) {
|
||||
_viewState.update {
|
||||
it.copy(length = value.toInt())
|
||||
}
|
||||
}
|
||||
|
||||
fun onUppercaseCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeUppercaseLetters = checked)
|
||||
}
|
||||
}
|
||||
|
||||
fun onLowercaseCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeLowercaseLetters = checked)
|
||||
}
|
||||
}
|
||||
|
||||
fun onNumbersCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeNumbers = checked)
|
||||
}
|
||||
}
|
||||
|
||||
fun onSymbolsCheckedChange(checked: Boolean) {
|
||||
_viewState.update {
|
||||
it.copy(includeSymbols = checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate
|
||||
|
||||
data class GeneratePasswordViewState(
|
||||
val length: Int,
|
||||
val includeUppercaseLetters: Boolean,
|
||||
val includeLowercaseLetters: Boolean,
|
||||
val includeSymbols: Boolean,
|
||||
val includeNumbers: Boolean,
|
||||
val password: String
|
||||
) {
|
||||
companion object {
|
||||
val Initial = GeneratePasswordViewState(
|
||||
length = 10,
|
||||
includeUppercaseLetters = true,
|
||||
includeLowercaseLetters = true,
|
||||
includeSymbols = true,
|
||||
includeNumbers = true,
|
||||
password = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate
|
||||
|
||||
data class GeneratePasswordViewState(
|
||||
val length: Int,
|
||||
val includeUppercaseLetters: Boolean,
|
||||
val includeLowercaseLetters: Boolean,
|
||||
val includeSymbols: Boolean,
|
||||
val includeNumbers: Boolean,
|
||||
val password: String
|
||||
) {
|
||||
companion object {
|
||||
val Initial = GeneratePasswordViewState(
|
||||
length = 10,
|
||||
includeUppercaseLetters = true,
|
||||
includeLowercaseLetters = true,
|
||||
includeSymbols = true,
|
||||
includeNumbers = true,
|
||||
password = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,226 +1,226 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewState
|
||||
import com.yogeshpaliyal.keypass.ui.generate.ui.components.CheckboxWithLabel
|
||||
|
||||
@Composable
|
||||
fun GeneratePasswordContent(
|
||||
viewState: GeneratePasswordViewState,
|
||||
onCopyPasswordClick: () -> Unit,
|
||||
onGeneratePasswordClick: () -> Unit,
|
||||
onPasswordLengthChange: (Float) -> Unit,
|
||||
onUppercaseCheckedChange: (Boolean) -> Unit,
|
||||
onLowercaseCheckedChange: (Boolean) -> Unit,
|
||||
onNumbersCheckedChange: (Boolean) -> Unit,
|
||||
onSymbolsCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
floatingActionButton = { GeneratePasswordFab(onGeneratePasswordClick) }
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
FormInputCard(
|
||||
viewState = viewState,
|
||||
onCopyPasswordClick = onCopyPasswordClick,
|
||||
onPasswordLengthChange = onPasswordLengthChange,
|
||||
onUppercaseCheckedChange = onUppercaseCheckedChange,
|
||||
onLowercaseCheckedChange = onLowercaseCheckedChange,
|
||||
onNumbersCheckedChange = onNumbersCheckedChange,
|
||||
onSymbolsCheckedChange = onSymbolsCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GeneratePasswordFab(onGeneratePasswordClick: () -> Unit) {
|
||||
FloatingActionButton(
|
||||
onClick = onGeneratePasswordClick,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FormInputCard(
|
||||
viewState: GeneratePasswordViewState,
|
||||
onCopyPasswordClick: () -> Unit,
|
||||
onPasswordLengthChange: (Float) -> Unit,
|
||||
onUppercaseCheckedChange: (Boolean) -> Unit,
|
||||
onLowercaseCheckedChange: (Boolean) -> Unit,
|
||||
onNumbersCheckedChange: (Boolean) -> Unit,
|
||||
onSymbolsCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
OutlinedCard(
|
||||
colors = CardDefaults.outlinedCardColors(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
PasswordTextField(viewState.password, onCopyPasswordClick)
|
||||
|
||||
// temporary label until we put slider label on the thumb to display current value.
|
||||
PasswordLengthInput(viewState.length, onPasswordLengthChange)
|
||||
|
||||
UppercaseAlphabetInput(viewState.includeUppercaseLetters, onUppercaseCheckedChange)
|
||||
|
||||
LowercaseAlphabetInput(viewState.includeLowercaseLetters, onLowercaseCheckedChange)
|
||||
|
||||
NumberInput(viewState.includeNumbers, onNumbersCheckedChange)
|
||||
|
||||
SymbolInput(viewState.includeSymbols, onSymbolsCheckedChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordTextField(
|
||||
password: String,
|
||||
onCopyPasswordClick: () -> Unit
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = onCopyPasswordClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy password"
|
||||
)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(text = "Password")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordLengthInput(
|
||||
length: Int,
|
||||
onPasswordLengthChange: (Float) -> Unit
|
||||
) {
|
||||
Text(text = "Password Length: $length")
|
||||
|
||||
Slider(
|
||||
value = length.toFloat(),
|
||||
onValueChange = onPasswordLengthChange,
|
||||
valueRange = 7f..50f,
|
||||
steps = 43
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UppercaseAlphabetInput(
|
||||
includeUppercaseLetters: Boolean,
|
||||
onUppercaseCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Uppercase Alphabets",
|
||||
checked = includeUppercaseLetters,
|
||||
onCheckedChange = onUppercaseCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LowercaseAlphabetInput(
|
||||
includeLowercaseLetters: Boolean,
|
||||
onLowercaseCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Lowercase Alphabets",
|
||||
checked = includeLowercaseLetters,
|
||||
onCheckedChange = onLowercaseCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NumberInput(
|
||||
includeNumbers: Boolean,
|
||||
onNumbersCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Numbers",
|
||||
checked = includeNumbers,
|
||||
onCheckedChange = onNumbersCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SymbolInput(
|
||||
includeSymbols: Boolean,
|
||||
onSymbolsCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Symbols",
|
||||
checked = includeSymbols,
|
||||
onCheckedChange = onSymbolsCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "Night Mode",
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Preview(
|
||||
name = "Day Mode",
|
||||
uiMode = Configuration.UI_MODE_NIGHT_NO
|
||||
)
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember", "MagicNumber")
|
||||
private fun GeneratePasswordContentPreview() {
|
||||
val viewState = GeneratePasswordViewState.Initial
|
||||
|
||||
Mdc3Theme {
|
||||
GeneratePasswordContent(
|
||||
viewState = viewState,
|
||||
onGeneratePasswordClick = {},
|
||||
onCopyPasswordClick = {},
|
||||
onPasswordLengthChange = {},
|
||||
onUppercaseCheckedChange = {},
|
||||
onLowercaseCheckedChange = {},
|
||||
onNumbersCheckedChange = {},
|
||||
onSymbolsCheckedChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewState
|
||||
import com.yogeshpaliyal.keypass.ui.generate.ui.components.CheckboxWithLabel
|
||||
|
||||
@Composable
|
||||
fun GeneratePasswordContent(
|
||||
viewState: GeneratePasswordViewState,
|
||||
onCopyPasswordClick: () -> Unit,
|
||||
onGeneratePasswordClick: () -> Unit,
|
||||
onPasswordLengthChange: (Float) -> Unit,
|
||||
onUppercaseCheckedChange: (Boolean) -> Unit,
|
||||
onLowercaseCheckedChange: (Boolean) -> Unit,
|
||||
onNumbersCheckedChange: (Boolean) -> Unit,
|
||||
onSymbolsCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
Scaffold(
|
||||
floatingActionButton = { GeneratePasswordFab(onGeneratePasswordClick) }
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
FormInputCard(
|
||||
viewState = viewState,
|
||||
onCopyPasswordClick = onCopyPasswordClick,
|
||||
onPasswordLengthChange = onPasswordLengthChange,
|
||||
onUppercaseCheckedChange = onUppercaseCheckedChange,
|
||||
onLowercaseCheckedChange = onLowercaseCheckedChange,
|
||||
onNumbersCheckedChange = onNumbersCheckedChange,
|
||||
onSymbolsCheckedChange = onSymbolsCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GeneratePasswordFab(onGeneratePasswordClick: () -> Unit) {
|
||||
FloatingActionButton(
|
||||
onClick = onGeneratePasswordClick,
|
||||
shape = RoundedCornerShape(16.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Refresh,
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FormInputCard(
|
||||
viewState: GeneratePasswordViewState,
|
||||
onCopyPasswordClick: () -> Unit,
|
||||
onPasswordLengthChange: (Float) -> Unit,
|
||||
onUppercaseCheckedChange: (Boolean) -> Unit,
|
||||
onLowercaseCheckedChange: (Boolean) -> Unit,
|
||||
onNumbersCheckedChange: (Boolean) -> Unit,
|
||||
onSymbolsCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
OutlinedCard(
|
||||
colors = CardDefaults.outlinedCardColors(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
PasswordTextField(viewState.password, onCopyPasswordClick)
|
||||
|
||||
// temporary label until we put slider label on the thumb to display current value.
|
||||
PasswordLengthInput(viewState.length, onPasswordLengthChange)
|
||||
|
||||
UppercaseAlphabetInput(viewState.includeUppercaseLetters, onUppercaseCheckedChange)
|
||||
|
||||
LowercaseAlphabetInput(viewState.includeLowercaseLetters, onLowercaseCheckedChange)
|
||||
|
||||
NumberInput(viewState.includeNumbers, onNumbersCheckedChange)
|
||||
|
||||
SymbolInput(viewState.includeSymbols, onSymbolsCheckedChange)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordTextField(
|
||||
password: String,
|
||||
onCopyPasswordClick: () -> Unit
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = {},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
readOnly = true,
|
||||
trailingIcon = {
|
||||
IconButton(
|
||||
onClick = onCopyPasswordClick
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy password"
|
||||
)
|
||||
}
|
||||
},
|
||||
label = {
|
||||
Text(text = "Password")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PasswordLengthInput(
|
||||
length: Int,
|
||||
onPasswordLengthChange: (Float) -> Unit
|
||||
) {
|
||||
Text(text = "Password Length: $length")
|
||||
|
||||
Slider(
|
||||
value = length.toFloat(),
|
||||
onValueChange = onPasswordLengthChange,
|
||||
valueRange = 7f..50f,
|
||||
steps = 43
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UppercaseAlphabetInput(
|
||||
includeUppercaseLetters: Boolean,
|
||||
onUppercaseCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Uppercase Alphabets",
|
||||
checked = includeUppercaseLetters,
|
||||
onCheckedChange = onUppercaseCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LowercaseAlphabetInput(
|
||||
includeLowercaseLetters: Boolean,
|
||||
onLowercaseCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Lowercase Alphabets",
|
||||
checked = includeLowercaseLetters,
|
||||
onCheckedChange = onLowercaseCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NumberInput(
|
||||
includeNumbers: Boolean,
|
||||
onNumbersCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Numbers",
|
||||
checked = includeNumbers,
|
||||
onCheckedChange = onNumbersCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SymbolInput(
|
||||
includeSymbols: Boolean,
|
||||
onSymbolsCheckedChange: (Boolean) -> Unit
|
||||
) {
|
||||
CheckboxWithLabel(
|
||||
label = "Symbols",
|
||||
checked = includeSymbols,
|
||||
onCheckedChange = onSymbolsCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(
|
||||
name = "Night Mode",
|
||||
uiMode = Configuration.UI_MODE_NIGHT_YES
|
||||
)
|
||||
@Preview(
|
||||
name = "Day Mode",
|
||||
uiMode = Configuration.UI_MODE_NIGHT_NO
|
||||
)
|
||||
@Composable
|
||||
@Suppress("UnusedPrivateMember", "MagicNumber")
|
||||
private fun GeneratePasswordContentPreview() {
|
||||
val viewState = GeneratePasswordViewState.Initial
|
||||
|
||||
Mdc3Theme {
|
||||
GeneratePasswordContent(
|
||||
viewState = viewState,
|
||||
onGeneratePasswordClick = {},
|
||||
onCopyPasswordClick = {},
|
||||
onPasswordLengthChange = {},
|
||||
onUppercaseCheckedChange = {},
|
||||
onLowercaseCheckedChange = {},
|
||||
onNumbersCheckedChange = {},
|
||||
onSymbolsCheckedChange = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewModel
|
||||
import com.yogeshpaliyal.keypass.ui.generate.ui.utils.copyTextToClipboard
|
||||
|
||||
@Composable
|
||||
fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// replace collectAsState() with collectAsStateWithLifecycle() when compose version and kotlin version are bumped up.
|
||||
val viewState by viewModel.viewState.collectAsState()
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
viewModel.generatePassword()
|
||||
}
|
||||
|
||||
GeneratePasswordContent(
|
||||
viewState = viewState,
|
||||
onGeneratePasswordClick = viewModel::generatePassword,
|
||||
onCopyPasswordClick = { onCopyPasswordClick(context, viewState.password) },
|
||||
onPasswordLengthChange = viewModel::onPasswordLengthSliderChange,
|
||||
onUppercaseCheckedChange = viewModel::onUppercaseCheckedChange,
|
||||
onLowercaseCheckedChange = viewModel::onLowercaseCheckedChange,
|
||||
onNumbersCheckedChange = viewModel::onNumbersCheckedChange,
|
||||
onSymbolsCheckedChange = viewModel::onSymbolsCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCopyPasswordClick(context: Context, text: String) {
|
||||
copyTextToClipboard(context = context, text = text, label = "random_password")
|
||||
Toast
|
||||
.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordViewModel
|
||||
import com.yogeshpaliyal.keypass.ui.generate.ui.utils.copyTextToClipboard
|
||||
|
||||
@Composable
|
||||
fun GeneratePasswordScreen(viewModel: GeneratePasswordViewModel) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// replace collectAsState() with collectAsStateWithLifecycle() when compose version and kotlin version are bumped up.
|
||||
val viewState by viewModel.viewState.collectAsState()
|
||||
|
||||
LaunchedEffect(key1 = Unit) {
|
||||
viewModel.generatePassword()
|
||||
}
|
||||
|
||||
GeneratePasswordContent(
|
||||
viewState = viewState,
|
||||
onGeneratePasswordClick = viewModel::generatePassword,
|
||||
onCopyPasswordClick = { onCopyPasswordClick(context, viewState.password) },
|
||||
onPasswordLengthChange = viewModel::onPasswordLengthSliderChange,
|
||||
onUppercaseCheckedChange = viewModel::onUppercaseCheckedChange,
|
||||
onLowercaseCheckedChange = viewModel::onLowercaseCheckedChange,
|
||||
onNumbersCheckedChange = viewModel::onNumbersCheckedChange,
|
||||
onSymbolsCheckedChange = viewModel::onSymbolsCheckedChange
|
||||
)
|
||||
}
|
||||
|
||||
private fun onCopyPasswordClick(context: Context, text: String) {
|
||||
copyTextToClipboard(context = context, text = text, label = "random_password")
|
||||
Toast
|
||||
.makeText(context, context.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun CheckboxWithLabel(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!checked) },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun CheckboxWithLabel(
|
||||
label: String,
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onCheckedChange(!checked) },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
fun copyTextToClipboard(
|
||||
context: Context,
|
||||
text: String,
|
||||
label: String
|
||||
) {
|
||||
val clipboard =
|
||||
context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(label, text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.generate.ui.utils
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
fun copyTextToClipboard(
|
||||
context: Context,
|
||||
text: String,
|
||||
label: String
|
||||
) {
|
||||
val clipboard =
|
||||
context.getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(label, text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
}
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 30-01-2021 23:02
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
application: Application,
|
||||
val appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
|
||||
private val appDao = appDb.getDao()
|
||||
|
||||
val mediator = MediatorLiveData<List<AccountModel>>()
|
||||
|
||||
fun queryUpdated(
|
||||
keyword: String?,
|
||||
tag: String?,
|
||||
sortField: String?,
|
||||
sortAscending: Boolean = true
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (sortAscending) {
|
||||
mediator.postValue(appDao.getAllAccountsAscending(keyword ?: "", tag, sortField))
|
||||
} else {
|
||||
mediator.postValue(appDao.getAllAccountsDescending(keyword ?: "", tag, sortField))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreBackup(
|
||||
keyphrase: String,
|
||||
contentResolver: ContentResolver,
|
||||
fileUri: Uri?
|
||||
): Boolean {
|
||||
return appDb.restoreBackup(keyphrase, contentResolver, fileUri)
|
||||
}
|
||||
}
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 30-01-2021 23:02
|
||||
*/
|
||||
@HiltViewModel
|
||||
class DashboardViewModel @Inject constructor(
|
||||
application: Application,
|
||||
val appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
) :
|
||||
AndroidViewModel(application) {
|
||||
|
||||
private val appDao = appDb.getDao()
|
||||
|
||||
val mediator = MediatorLiveData<List<AccountModel>>()
|
||||
|
||||
fun queryUpdated(
|
||||
keyword: String?,
|
||||
tag: String?,
|
||||
sortField: String?,
|
||||
sortAscending: Boolean = true
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (sortAscending) {
|
||||
mediator.postValue(appDao.getAllAccountsAscending(keyword ?: "", tag, sortField))
|
||||
} else {
|
||||
mediator.postValue(appDao.getAllAccountsDescending(keyword ?: "", tag, sortField))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun restoreBackup(
|
||||
keyphrase: String,
|
||||
contentResolver: ContentResolver,
|
||||
fileUri: Uri?
|
||||
): Boolean {
|
||||
return appDb.restoreBackup(keyphrase, contentResolver, fileUri)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,96 @@
|
||||
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
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.yogeshpaliyal.keypass.ui.home.components.AccountsList
|
||||
import com.yogeshpaliyal.keypass.ui.home.components.SearchBar
|
||||
import com.yogeshpaliyal.keypass.ui.redux.HomeState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.StateUpdateAction
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 09:25
|
||||
*/
|
||||
|
||||
@Composable()
|
||||
fun Homepage(
|
||||
mViewModel: DashboardViewModel = viewModel(),
|
||||
homeState: HomeState
|
||||
) {
|
||||
val tag = homeState.tag
|
||||
val keyword = homeState.keyword
|
||||
val sortField = homeState.sortField
|
||||
val sortAscendingOrder = homeState.sortAscending
|
||||
|
||||
val listOfAccountsLiveData by mViewModel.mediator.observeAsState()
|
||||
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
LaunchedEffect(tag, keyword, sortField, sortAscendingOrder, block = {
|
||||
mViewModel.queryUpdated(keyword, tag, sortField, sortAscendingOrder)
|
||||
})
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
SearchBar(keyword, {
|
||||
dispatchAction(StateUpdateAction(homeState.copy(keyword = it)))
|
||||
}) { field, order ->
|
||||
dispatchAction(
|
||||
|
||||
StateUpdateAction(
|
||||
homeState.copy(
|
||||
sortField = field.value,
|
||||
sortAscending = order == SortingOrder.Ascending
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(tag != null) {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
content = {
|
||||
item {
|
||||
AssistChip(onClick = { }, label = {
|
||||
Text(text = tag ?: "")
|
||||
}, trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
dispatchAction(NavigationAction(HomeState(), true))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Close),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AccountsList(listOfAccountsLiveData)
|
||||
}
|
||||
}
|
||||
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
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material3.AssistChip
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.yogeshpaliyal.keypass.ui.home.components.AccountsList
|
||||
import com.yogeshpaliyal.keypass.ui.home.components.SearchBar
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 09:25
|
||||
*/
|
||||
|
||||
@Composable()
|
||||
fun Homepage(
|
||||
mViewModel: DashboardViewModel = viewModel(),
|
||||
homeState: HomeState
|
||||
) {
|
||||
val tag = homeState.tag
|
||||
val keyword = homeState.keyword
|
||||
val sortField = homeState.sortField
|
||||
val sortAscendingOrder = homeState.sortAscending
|
||||
|
||||
val listOfAccountsLiveData by mViewModel.mediator.observeAsState()
|
||||
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
LaunchedEffect(tag, keyword, sortField, sortAscendingOrder, block = {
|
||||
mViewModel.queryUpdated(keyword, tag, sortField, sortAscendingOrder)
|
||||
})
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
SearchBar(keyword, {
|
||||
dispatchAction(StateUpdateAction(homeState.copy(keyword = it)))
|
||||
}) { field, order ->
|
||||
dispatchAction(
|
||||
|
||||
StateUpdateAction(
|
||||
homeState.copy(
|
||||
sortField = field.value,
|
||||
sortAscending = order == SortingOrder.Ascending
|
||||
)
|
||||
)
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(tag != null) {
|
||||
LazyRow(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
content = {
|
||||
item {
|
||||
AssistChip(onClick = { }, label = {
|
||||
Text(text = tag ?: "")
|
||||
}, trailingIcon = {
|
||||
IconButton(onClick = {
|
||||
dispatchAction(NavigationAction(HomeState(), true))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Close),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AccountsList(listOfAccountsLiveData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package com.yogeshpaliyal.keypass.ui.home
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
|
||||
sealed class SortingOrder(@StringRes val label: Int) {
|
||||
object Ascending : SortingOrder(R.string.ascending)
|
||||
object Descending : SortingOrder(R.string.descending)
|
||||
}
|
||||
|
||||
fun getSortingOrderOptions() = mutableListOf(SortingOrder.Ascending, SortingOrder.Descending)
|
||||
|
||||
sealed class SortingField(
|
||||
@StringRes val label: Int,
|
||||
val value: String,
|
||||
val sortingOrders: List<SortingOrder>
|
||||
) {
|
||||
object Title : SortingField(R.string.account_name, "title", getSortingOrderOptions())
|
||||
object Username : SortingField(R.string.username_email_phone, "username", getSortingOrderOptions())
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.home
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
|
||||
sealed class SortingOrder(@StringRes val label: Int) {
|
||||
object Ascending : SortingOrder(R.string.ascending)
|
||||
object Descending : SortingOrder(R.string.descending)
|
||||
}
|
||||
|
||||
fun getSortingOrderOptions() = mutableListOf(SortingOrder.Ascending, SortingOrder.Descending)
|
||||
|
||||
sealed class SortingField(
|
||||
@StringRes val label: Int,
|
||||
val value: String,
|
||||
val sortingOrders: List<SortingOrder>
|
||||
) {
|
||||
object Title : SortingField(R.string.account_name, "title", getSortingOrderOptions())
|
||||
object Username : SortingField(R.string.username_email_phone, "username", getSortingOrderOptions())
|
||||
}
|
||||
|
||||
@@ -1,242 +1,242 @@
|
||||
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
|
||||
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.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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.ContentCopy
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.yogeshpaliyal.common.constants.AccountType
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.AccountDetailState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.CopyToClipboard
|
||||
import com.yogeshpaliyal.keypass.ui.redux.IntentNavigation
|
||||
import com.yogeshpaliyal.keypass.ui.redux.NavigationAction
|
||||
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) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NoDataFound()
|
||||
}
|
||||
}
|
||||
|
||||
@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) }
|
||||
) {
|
||||
Row(modifier = Modifier.padding(12.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(60.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = accountModel.getInitials(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (accountModel.type == AccountType.TOTP) {
|
||||
WrapWithProgress(accountModel)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = accountModel.title ?: "",
|
||||
style = MaterialTheme.typography.headlineSmall.merge(
|
||||
TextStyle(
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
RenderUserName(accountModel)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
onClick = { dispatch(CopyToClipboard(getPassword(accountModel))) }
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.TwoTone.ContentCopy),
|
||||
contentDescription = "Copy To Clipboard"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUsernameOrOtp(accountModel: AccountModel): String? {
|
||||
return if (accountModel.type == AccountType.TOTP) accountModel.getOtp() else accountModel.username
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderUserName(accountModel: AccountModel) {
|
||||
val (username, setUsername) = remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(accountModel) {
|
||||
if (accountModel.type == AccountType.TOTP) {
|
||||
while (true) {
|
||||
setUsername(accountModel.getOtp())
|
||||
delay(1.seconds)
|
||||
}
|
||||
} else {
|
||||
setUsername(accountModel.username ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = username,
|
||||
style = MaterialTheme.typography.bodyMedium.merge(
|
||||
TextStyle(
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoDataFound() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.message_no_accounts),
|
||||
modifier = Modifier
|
||||
.padding(32.dp)
|
||||
.align(alignment = Alignment.CenterHorizontally),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_undraw_empty_street_sfxm),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPassword(model: AccountModel): String {
|
||||
if (model.type == AccountType.TOTP) {
|
||||
return model.getOtp()
|
||||
}
|
||||
return model.password.orEmpty()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WrapWithProgress(accountModel: AccountModel) {
|
||||
if (accountModel.type != AccountType.TOTP) {
|
||||
return
|
||||
}
|
||||
|
||||
val infiniteTransition =
|
||||
rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "")
|
||||
val rotationAnimation = infiniteTransition.animateFloat(
|
||||
initialValue = 1f - (accountModel.getTOtpProgress().toFloat() / 30),
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(tween(30000, easing = LinearEasing))
|
||||
)
|
||||
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
progress = rotationAnimation.value
|
||||
)
|
||||
}
|
||||
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
|
||||
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.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.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.twotone.ContentCopy
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.yogeshpaliyal.common.constants.AccountType
|
||||
import com.yogeshpaliyal.common.data.AccountModel
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
|
||||
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 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) {
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
NoDataFound()
|
||||
}
|
||||
}
|
||||
|
||||
@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) }
|
||||
) {
|
||||
Row(modifier = Modifier.padding(12.dp)) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.size(60.dp)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f),
|
||||
shape = RoundedCornerShape(50)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
text = accountModel.getInitials(),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
if (accountModel.type == AccountType.TOTP) {
|
||||
WrapWithProgress(accountModel)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.align(Alignment.CenterVertically)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = accountModel.title ?: "",
|
||||
style = MaterialTheme.typography.headlineSmall.merge(
|
||||
TextStyle(
|
||||
fontSize = 16.sp
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
RenderUserName(accountModel)
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
onClick = { dispatch(CopyToClipboard(getPassword(accountModel))) }
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.TwoTone.ContentCopy),
|
||||
contentDescription = "Copy To Clipboard"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getUsernameOrOtp(accountModel: AccountModel): String? {
|
||||
return if (accountModel.type == AccountType.TOTP) accountModel.getOtp() else accountModel.username
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderUserName(accountModel: AccountModel) {
|
||||
val (username, setUsername) = remember { mutableStateOf("") }
|
||||
|
||||
LaunchedEffect(accountModel) {
|
||||
if (accountModel.type == AccountType.TOTP) {
|
||||
while (true) {
|
||||
setUsername(accountModel.getOtp())
|
||||
delay(1.seconds)
|
||||
}
|
||||
} else {
|
||||
setUsername(accountModel.username ?: "")
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = username,
|
||||
style = MaterialTheme.typography.bodyMedium.merge(
|
||||
TextStyle(
|
||||
fontSize = 14.sp
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoDataFound() {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.message_no_accounts),
|
||||
modifier = Modifier
|
||||
.padding(32.dp)
|
||||
.align(alignment = Alignment.CenterHorizontally),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_undraw_empty_street_sfxm),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPassword(model: AccountModel): String {
|
||||
if (model.type == AccountType.TOTP) {
|
||||
return model.getOtp()
|
||||
}
|
||||
return model.password.orEmpty()
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WrapWithProgress(accountModel: AccountModel) {
|
||||
if (accountModel.type != AccountType.TOTP) {
|
||||
return
|
||||
}
|
||||
|
||||
val infiniteTransition =
|
||||
rememberInfiniteTransition(accountModel.uniqueId ?: accountModel.title ?: "")
|
||||
val rotationAnimation = infiniteTransition.animateFloat(
|
||||
initialValue = 1f - (accountModel.getTOtpProgress().toFloat() / 30),
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(tween(30000, easing = LinearEasing))
|
||||
)
|
||||
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
progress = rotationAnimation.value
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,69 +1,69 @@
|
||||
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
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Sort
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingField
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingOrder
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
keyword: String?,
|
||||
updateKeyword: (keyword: String) -> Unit,
|
||||
updateSorting: (SortingField, SortingOrder) -> Unit
|
||||
) {
|
||||
val (isMenuVisible, setMenuVisible) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.padding(16.dp),
|
||||
value = keyword ?: "",
|
||||
placeholder = {
|
||||
Text(text = "Search Account")
|
||||
},
|
||||
onValueChange = { newValue ->
|
||||
updateKeyword(newValue)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
AnimatedVisibility(keyword.isNullOrBlank().not()) {
|
||||
IconButton(onClick = { updateKeyword("") }) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Close),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
setMenuVisible(!isMenuVisible)
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Sort),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SortingMenu(isMenuVisible, setMenuVisible) { sortingField, order ->
|
||||
updateSorting(sortingField, order)
|
||||
setMenuVisible(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
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
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Sort
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingField
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingOrder
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
keyword: String?,
|
||||
updateKeyword: (keyword: String) -> Unit,
|
||||
updateSorting: (SortingField, SortingOrder) -> Unit
|
||||
) {
|
||||
val (isMenuVisible, setMenuVisible) = rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.padding(16.dp),
|
||||
value = keyword ?: "",
|
||||
placeholder = {
|
||||
Text(text = "Search Account")
|
||||
},
|
||||
onValueChange = { newValue ->
|
||||
updateKeyword(newValue)
|
||||
},
|
||||
trailingIcon = {
|
||||
Row {
|
||||
AnimatedVisibility(keyword.isNullOrBlank().not()) {
|
||||
IconButton(onClick = { updateKeyword("") }) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Close),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
setMenuVisible(!isMenuVisible)
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Sort),
|
||||
contentDescription = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
SortingMenu(isMenuVisible, setMenuVisible) { sortingField, order ->
|
||||
updateSorting(sortingField, order)
|
||||
setMenuVisible(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
package com.yogeshpaliyal.keypass.ui.home.components
|
||||
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingField
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingOrder
|
||||
import me.saket.cascade.CascadeDropdownMenu
|
||||
import me.saket.cascade.rememberCascadeState
|
||||
|
||||
@Composable
|
||||
fun SortingMenu(
|
||||
isMenuVisible: Boolean,
|
||||
setMenuVisible: (Boolean) -> Unit,
|
||||
onOptionSelected: (SortingField, SortingOrder) -> Unit
|
||||
) {
|
||||
val state = rememberCascadeState()
|
||||
|
||||
val sortingOptions =
|
||||
remember { mutableListOf(SortingField.Title, SortingField.Username) }
|
||||
|
||||
CascadeDropdownMenu(
|
||||
state = state,
|
||||
expanded = isMenuVisible,
|
||||
onDismissRequest = { setMenuVisible(false) }
|
||||
) {
|
||||
sortingOptions.forEach { sortingField ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = sortingField.label)) },
|
||||
children = {
|
||||
sortingField.sortingOrders.forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = it.label)) },
|
||||
onClick = {
|
||||
onOptionSelected(sortingField, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.home.components
|
||||
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingField
|
||||
import com.yogeshpaliyal.keypass.ui.home.SortingOrder
|
||||
import me.saket.cascade.CascadeDropdownMenu
|
||||
import me.saket.cascade.rememberCascadeState
|
||||
|
||||
@Composable
|
||||
fun SortingMenu(
|
||||
isMenuVisible: Boolean,
|
||||
setMenuVisible: (Boolean) -> Unit,
|
||||
onOptionSelected: (SortingField, SortingOrder) -> Unit
|
||||
) {
|
||||
val state = rememberCascadeState()
|
||||
|
||||
val sortingOptions =
|
||||
remember { mutableListOf(SortingField.Title, SortingField.Username) }
|
||||
|
||||
CascadeDropdownMenu(
|
||||
state = state,
|
||||
expanded = isMenuVisible,
|
||||
onDismissRequest = { setMenuVisible(false) }
|
||||
) {
|
||||
sortingOptions.forEach { sortingField ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = sortingField.label)) },
|
||||
children = {
|
||||
sortingField.sortingOrders.forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(id = it.label)) },
|
||||
onClick = {
|
||||
onOptionSelected(sortingField, it)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 14:11
|
||||
*/
|
||||
@HiltViewModel
|
||||
class BottomNavViewModel @Inject constructor(
|
||||
application: Application,
|
||||
val appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
) : AndroidViewModel(application) {
|
||||
private val _navigationList: MutableLiveData<List<NavigationModelItem>> = MutableLiveData()
|
||||
private val tagsDb = appDb.getDao().getTags()
|
||||
|
||||
private var tagsList: List<String>? = null
|
||||
|
||||
val navigationList: LiveData<List<NavigationModelItem>>
|
||||
get() = _navigationList
|
||||
|
||||
init {
|
||||
postListUpdate()
|
||||
|
||||
viewModelScope.launch {
|
||||
tagsDb.collect {
|
||||
tagsList = it
|
||||
postListUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currently selected menu item.
|
||||
*
|
||||
* @return true if the currently selected item has changed.
|
||||
*/
|
||||
fun setNavigationMenuItemChecked(id: Int): Boolean {
|
||||
var updated = false
|
||||
NavigationModel.navigationMenuItems.forEachIndexed { index, item ->
|
||||
val shouldCheck = item.id == id
|
||||
if (item.checked != shouldCheck) {
|
||||
NavigationModel.navigationMenuItems[index] = item.copy(checked = shouldCheck)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
if (updated) postListUpdate()
|
||||
return updated
|
||||
}
|
||||
|
||||
private fun postListUpdate() {
|
||||
val newList = if (tagsList.isNullOrEmpty().not()) {
|
||||
NavigationModel.navigationMenuItems + NavigationModelItem.NavDivider("Tags") + (
|
||||
tagsList?.filter { it != null }
|
||||
?.map { NavigationModelItem.NavTagItem(it) } ?: listOf()
|
||||
)
|
||||
} else {
|
||||
NavigationModel.navigationMenuItems
|
||||
}
|
||||
|
||||
_navigationList.value = newList
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 31-01-2021 14:11
|
||||
*/
|
||||
@HiltViewModel
|
||||
class BottomNavViewModel @Inject constructor(
|
||||
application: Application,
|
||||
val appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
) : AndroidViewModel(application) {
|
||||
private val _navigationList: MutableLiveData<List<NavigationModelItem>> = MutableLiveData()
|
||||
private val tagsDb = appDb.getDao().getTags()
|
||||
|
||||
private var tagsList: List<String>? = null
|
||||
|
||||
val navigationList: LiveData<List<NavigationModelItem>>
|
||||
get() = _navigationList
|
||||
|
||||
init {
|
||||
postListUpdate()
|
||||
|
||||
viewModelScope.launch {
|
||||
tagsDb.collect {
|
||||
tagsList = it
|
||||
postListUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the currently selected menu item.
|
||||
*
|
||||
* @return true if the currently selected item has changed.
|
||||
*/
|
||||
fun setNavigationMenuItemChecked(id: Int): Boolean {
|
||||
var updated = false
|
||||
NavigationModel.navigationMenuItems.forEachIndexed { index, item ->
|
||||
val shouldCheck = item.id == id
|
||||
if (item.checked != shouldCheck) {
|
||||
NavigationModel.navigationMenuItems[index] = item.copy(checked = shouldCheck)
|
||||
updated = true
|
||||
}
|
||||
}
|
||||
if (updated) postListUpdate()
|
||||
return updated
|
||||
}
|
||||
|
||||
private fun postListUpdate() {
|
||||
val newList = if (tagsList.isNullOrEmpty().not()) {
|
||||
NavigationModel.navigationMenuItems + NavigationModelItem.NavDivider("Tags") + (
|
||||
tagsList?.filter { it != null }
|
||||
?.map { NavigationModelItem.NavTagItem(it) } ?: listOf()
|
||||
)
|
||||
} else {
|
||||
NavigationModel.navigationMenuItems
|
||||
}
|
||||
|
||||
_navigationList.value = newList
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,305 +1,312 @@
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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 com.yogeshpaliyal.keypass.BuildConfig
|
||||
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
|
||||
import com.yogeshpaliyal.keypass.ui.redux.HomeState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.KeyPassRedux
|
||||
import com.yogeshpaliyal.keypass.ui.redux.KeyPassState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.NavigationAction
|
||||
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
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
import org.reduxkotlin.compose.selectState
|
||||
import java.util.Locale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DashboardComposeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (BuildConfig.DEBUG.not()) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
setContent {
|
||||
KeyPassTheme {
|
||||
StoreProvider(store = KeyPassRedux.createStore()) {
|
||||
Dashboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Dashboard() {
|
||||
val systemBackPress by selectState<KeyPassState, Boolean> { this.systemBackPress }
|
||||
|
||||
val context = LocalContext.current
|
||||
val dispatch = rememberDispatcher()
|
||||
|
||||
BackHandler(!systemBackPress) {
|
||||
dispatch(GoBackAction)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = systemBackPress, block = {
|
||||
if (systemBackPress) {
|
||||
(context as? AppCompatActivity)?.onBackPressed()
|
||||
}
|
||||
})
|
||||
|
||||
DisposableEffect(KeyPassRedux, context) {
|
||||
dispatch(UpdateContextAction(context))
|
||||
|
||||
onDispose {
|
||||
dispatch(UpdateContextAction(null))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(bottomBar = {
|
||||
KeyPassBottomBar()
|
||||
}) { paddingValues ->
|
||||
Surface(modifier = Modifier.padding(paddingValues)) {
|
||||
CurrentPage()
|
||||
|
||||
OptionBottomBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrentPage() {
|
||||
val currentScreen by selectState<KeyPassState, ScreenState> { this.currentScreen }
|
||||
|
||||
currentScreen.let {
|
||||
when (it) {
|
||||
is HomeState -> {
|
||||
Homepage(homeState = it)
|
||||
}
|
||||
|
||||
is SettingsState -> {
|
||||
MySettingCompose()
|
||||
}
|
||||
|
||||
is AccountDetailState -> {
|
||||
AccountDetailPage(id = it.accountId)
|
||||
}
|
||||
|
||||
is AuthState -> {
|
||||
AuthScreen(it)
|
||||
}
|
||||
|
||||
is TotpDetailState -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionBottomBar(
|
||||
viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
) {
|
||||
val bottomSheetState by selectState<KeyPassState, BottomSheetState?> { this.bottomSheet }
|
||||
|
||||
if (bottomSheetState?.isBottomSheetOpen != true) {
|
||||
return
|
||||
}
|
||||
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
val navigationItems by viewModel.navigationList.observeAsState()
|
||||
ModalBottomSheet(onDismissRequest = {
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(false))
|
||||
}) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (navigationItems != null) {
|
||||
items(navigationItems!!) {
|
||||
when (it) {
|
||||
is NavigationModelItem.NavDivider -> {
|
||||
NavItemSection(it)
|
||||
}
|
||||
|
||||
is NavigationModelItem.NavTagItem -> {
|
||||
NavMenuFolder(folder = it) {
|
||||
dispatchAction(NavigationAction(HomeState(tag = it.tag), false))
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(false))
|
||||
}
|
||||
}
|
||||
|
||||
is NavigationModelItem.NavMenuItem -> {
|
||||
NavItem(it) {
|
||||
dispatchAction(it.action)
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
) {
|
||||
Text(
|
||||
text = folder.tag,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavItem(item: NavigationModelItem.NavMenuItem, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true),
|
||||
onClick = onClick
|
||||
)
|
||||
.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavItemSection(divider: NavigationModelItem.NavDivider) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text(
|
||||
text = divider.title.uppercase(Locale.getDefault()),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontSize = TextUnit(12f, TextUnitType.Sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyPassBottomBar() {
|
||||
val showMainBottomAppBar by selectState<KeyPassState, Boolean> { this.currentScreen.showMainBottomAppBar }
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
if (!showMainBottomAppBar) {
|
||||
return
|
||||
}
|
||||
|
||||
BottomAppBar(actions = {
|
||||
IconButton(onClick = {
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(true))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Menu),
|
||||
contentDescription = "Menu",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
dispatchAction(NavigationAction(SettingsState))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Settings),
|
||||
contentDescription = "Settings",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}, floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
dispatchAction(NavigationAction(AccountDetailState()))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Add),
|
||||
contentDescription = "Add"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
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.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.Menu
|
||||
import androidx.compose.material.icons.rounded.Settings
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.BottomAppBar
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.Scaffold
|
||||
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
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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 com.yogeshpaliyal.keypass.BuildConfig
|
||||
import com.yogeshpaliyal.keypass.ui.auth.AuthScreen
|
||||
import com.yogeshpaliyal.keypass.ui.backup.BackupScreen
|
||||
import com.yogeshpaliyal.keypass.ui.detail.AccountDetailPage
|
||||
import com.yogeshpaliyal.keypass.ui.home.Homepage
|
||||
import com.yogeshpaliyal.keypass.ui.redux.KeyPassRedux
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.BottomSheetAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.AccountDetailState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.AuthState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BottomSheetState
|
||||
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 com.yogeshpaliyal.keypass.ui.redux.states.TotpDetailState
|
||||
import com.yogeshpaliyal.keypass.ui.settings.MySettingCompose
|
||||
import com.yogeshpaliyal.keypass.ui.style.KeyPassTheme
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.reduxkotlin.compose.StoreProvider
|
||||
import org.reduxkotlin.compose.rememberDispatcher
|
||||
import org.reduxkotlin.compose.selectState
|
||||
import java.util.Locale
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DashboardComposeActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (BuildConfig.DEBUG.not()) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
setContent {
|
||||
KeyPassTheme {
|
||||
StoreProvider(store = KeyPassRedux.createStore()) {
|
||||
Dashboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Dashboard() {
|
||||
val systemBackPress by selectState<KeyPassState, Boolean> { this.systemBackPress }
|
||||
|
||||
val context = LocalContext.current
|
||||
val dispatch = rememberDispatcher()
|
||||
|
||||
BackHandler(!systemBackPress) {
|
||||
dispatch(GoBackAction)
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = systemBackPress, block = {
|
||||
if (systemBackPress) {
|
||||
(context as? AppCompatActivity)?.onBackPressed()
|
||||
}
|
||||
})
|
||||
|
||||
DisposableEffect(KeyPassRedux, context) {
|
||||
dispatch(UpdateContextAction(context))
|
||||
|
||||
onDispose {
|
||||
dispatch(UpdateContextAction(null))
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(bottomBar = {
|
||||
KeyPassBottomBar()
|
||||
}) { paddingValues ->
|
||||
Surface(modifier = Modifier.padding(paddingValues)) {
|
||||
CurrentPage()
|
||||
|
||||
OptionBottomBar()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrentPage() {
|
||||
val currentScreen by selectState<KeyPassState, ScreenState> { this.currentScreen }
|
||||
|
||||
currentScreen.let {
|
||||
when (it) {
|
||||
is HomeState -> {
|
||||
Homepage(homeState = it)
|
||||
}
|
||||
|
||||
is SettingsState -> {
|
||||
MySettingCompose()
|
||||
}
|
||||
|
||||
is AccountDetailState -> {
|
||||
AccountDetailPage(id = it.accountId)
|
||||
}
|
||||
|
||||
is AuthState -> {
|
||||
AuthScreen(it)
|
||||
}
|
||||
|
||||
is BackupScreenState -> {
|
||||
BackupScreen(state = it)
|
||||
}
|
||||
|
||||
is TotpDetailState -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OptionBottomBar(
|
||||
viewModel: BottomNavViewModel = androidx.lifecycle.viewmodel.compose.viewModel()
|
||||
) {
|
||||
val bottomSheetState by selectState<KeyPassState, BottomSheetState?> { this.bottomSheet }
|
||||
|
||||
if (bottomSheetState?.isBottomSheetOpen != true) {
|
||||
return
|
||||
}
|
||||
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
val navigationItems by viewModel.navigationList.observeAsState()
|
||||
ModalBottomSheet(onDismissRequest = {
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(false))
|
||||
}) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
if (navigationItems != null) {
|
||||
items(navigationItems!!) {
|
||||
when (it) {
|
||||
is NavigationModelItem.NavDivider -> {
|
||||
NavItemSection(it)
|
||||
}
|
||||
|
||||
is NavigationModelItem.NavTagItem -> {
|
||||
NavMenuFolder(folder = it) {
|
||||
dispatchAction(NavigationAction(HomeState(tag = it.tag), false))
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(false))
|
||||
}
|
||||
}
|
||||
|
||||
is NavigationModelItem.NavMenuItem -> {
|
||||
NavItem(it) {
|
||||
dispatchAction(it.action)
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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)
|
||||
|
||||
) {
|
||||
Text(
|
||||
text = folder.tag,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavItem(item: NavigationModelItem.NavMenuItem, onClick: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = true),
|
||||
onClick = onClick
|
||||
)
|
||||
.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavItemSection(divider: NavigationModelItem.NavDivider) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text(
|
||||
text = divider.title.uppercase(Locale.getDefault()),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontSize = TextUnit(12f, TextUnitType.Sp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun KeyPassBottomBar() {
|
||||
val showMainBottomAppBar by selectState<KeyPassState, Boolean> { this.currentScreen.showMainBottomAppBar }
|
||||
val dispatchAction = rememberDispatcher()
|
||||
|
||||
if (!showMainBottomAppBar) {
|
||||
return
|
||||
}
|
||||
|
||||
BottomAppBar(actions = {
|
||||
IconButton(onClick = {
|
||||
dispatchAction(BottomSheetAction.HomeNavigationMenu(true))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Menu),
|
||||
contentDescription = "Menu",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = {
|
||||
dispatchAction(NavigationAction(SettingsState))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Settings),
|
||||
contentDescription = "Settings",
|
||||
tint = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}, floatingActionButton = {
|
||||
FloatingActionButton(modifier = Modifier.testTag("btnAdd"), onClick = {
|
||||
dispatchAction(NavigationAction(AccountDetailState()))
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(image = Icons.Rounded.Add),
|
||||
contentDescription = "Add"
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.HomeState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.IntentNavigation
|
||||
import com.yogeshpaliyal.keypass.ui.redux.NavigationAction
|
||||
|
||||
object NavigationModel {
|
||||
|
||||
const val HOME = 0
|
||||
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
|
||||
),
|
||||
NavigationModelItem.NavMenuItem(
|
||||
id = ADD_TOPT,
|
||||
icon = R.drawable.ic_twotone_totp,
|
||||
titleRes = R.string.add_totp,
|
||||
checked = false,
|
||||
action = IntentNavigation.AddTOTP()
|
||||
)
|
||||
)
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.HomeState
|
||||
|
||||
object NavigationModel {
|
||||
|
||||
const val HOME = 0
|
||||
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
|
||||
),
|
||||
NavigationModelItem.NavMenuItem(
|
||||
id = ADD_TOPT,
|
||||
icon = R.drawable.ic_twotone_totp,
|
||||
titleRes = R.string.add_totp,
|
||||
checked = false,
|
||||
action = IntentNavigation.AddTOTP()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.yogeshpaliyal.keypass.ui.redux.Action
|
||||
|
||||
sealed class NavigationModelItem {
|
||||
|
||||
data class NavMenuItem(
|
||||
val id: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
@StringRes val titleRes: Int,
|
||||
var checked: Boolean,
|
||||
var action: Action
|
||||
) : 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 NavTagItem(val tag: String) : NavigationModelItem()
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.nav
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
|
||||
sealed class NavigationModelItem {
|
||||
|
||||
data class NavMenuItem(
|
||||
val id: Int,
|
||||
@DrawableRes val icon: Int,
|
||||
@StringRes val titleRes: Int,
|
||||
var checked: Boolean,
|
||||
var action: Action
|
||||
) : 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 NavTagItem(val tag: String) : NavigationModelItem()
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
sealed interface Action
|
||||
|
||||
class UpdateContextAction(val context: Context?) : Action
|
||||
|
||||
object GoBackAction : Action
|
||||
|
||||
data class NavigationAction(val state: ScreenState, val clearBackStack: Boolean = false) : Action
|
||||
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
|
||||
}
|
||||
|
||||
data class ToastAction(@StringRes val text: Int) : Action
|
||||
data class CopyToClipboard(val password: String) : Action
|
||||
|
||||
sealed class BottomSheetAction(
|
||||
val route: String,
|
||||
val globalIsBottomSheetOpen: Boolean,
|
||||
val globalArgs: Bundle? = null
|
||||
) : Action {
|
||||
data class HomeNavigationMenu(val isBottomSheetOpen: Boolean, val args: Bundle? = null) :
|
||||
BottomSheetAction(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen, args)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux
|
||||
|
||||
object BottomSheetRoutes {
|
||||
const val HOME_NAV_MENU = "/home/navMenu"
|
||||
}
|
||||
@@ -1,124 +1,103 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux
|
||||
|
||||
import android.content.ClipData
|
||||
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
|
||||
import org.reduxkotlin.createStore
|
||||
import org.reduxkotlin.middleware
|
||||
|
||||
object KeyPassRedux {
|
||||
|
||||
private var arrPages = mutableListOf<ScreenState>()
|
||||
|
||||
private val reducer: Reducer<KeyPassState> = { state, action ->
|
||||
when (action) {
|
||||
is NavigationAction -> {
|
||||
if (action.clearBackStack) {
|
||||
arrPages.clear()
|
||||
} else {
|
||||
arrPages.add(state.currentScreen)
|
||||
}
|
||||
|
||||
state.copy(currentScreen = action.state)
|
||||
}
|
||||
|
||||
is StateUpdateAction -> {
|
||||
state.copy(currentScreen = action.state)
|
||||
}
|
||||
|
||||
is CopyToClipboard -> {
|
||||
state.context?.let {
|
||||
val clipboard = ContextCompat.getSystemService(
|
||||
it,
|
||||
ClipboardManager::class.java
|
||||
)
|
||||
val clip = ClipData.newPlainText("KeyPass", action.password)
|
||||
clipboard?.setPrimaryClip(clip)
|
||||
|
||||
Toast.makeText(
|
||||
it,
|
||||
R.string.copied_to_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
is UpdateContextAction -> {
|
||||
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) {
|
||||
state.copy(currentScreen = lastItem)
|
||||
} else {
|
||||
state.copy(systemBackPress = true)
|
||||
}
|
||||
}
|
||||
|
||||
is BottomSheetAction -> {
|
||||
state.copy(
|
||||
bottomSheet = BottomSheetState(
|
||||
action.route,
|
||||
action.globalArgs,
|
||||
action.globalIsBottomSheetOpen
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
private val intentNavigationMiddleware = middleware<KeyPassState> { store, next, action ->
|
||||
val state = store.state
|
||||
|
||||
when (action) {
|
||||
is IntentNavigation.GeneratePassword -> {
|
||||
val intent = Intent(state.context, GeneratePasswordActivity::class.java)
|
||||
state.context?.startActivity(intent)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fun createStore() =
|
||||
createStore(reducer, generateDefaultState(), applyMiddleware(intentNavigationMiddleware))
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.redux
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.BottomSheetAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.CopyToClipboard
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.GoBackAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.StateUpdateAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.UpdateContextAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.middlewares.intentNavigationMiddleware
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BottomSheetState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.generateDefaultState
|
||||
import org.reduxkotlin.Reducer
|
||||
import org.reduxkotlin.applyMiddleware
|
||||
import org.reduxkotlin.createStore
|
||||
|
||||
object KeyPassRedux {
|
||||
|
||||
private var arrPages = mutableListOf<ScreenState>()
|
||||
|
||||
private val reducer: Reducer<KeyPassState> = { state, action ->
|
||||
when (action) {
|
||||
is NavigationAction -> {
|
||||
if (action.clearBackStack) {
|
||||
arrPages.clear()
|
||||
} else {
|
||||
arrPages.add(state.currentScreen)
|
||||
}
|
||||
|
||||
state.copy(currentScreen = action.state)
|
||||
}
|
||||
|
||||
is StateUpdateAction -> {
|
||||
state.copy(currentScreen = action.state)
|
||||
}
|
||||
|
||||
is CopyToClipboard -> {
|
||||
state.context?.let {
|
||||
val clipboard = ContextCompat.getSystemService(
|
||||
it,
|
||||
ClipboardManager::class.java
|
||||
)
|
||||
val clip = ClipData.newPlainText("KeyPass", action.password)
|
||||
clipboard?.setPrimaryClip(clip)
|
||||
|
||||
Toast.makeText(
|
||||
it,
|
||||
R.string.copied_to_clipboard,
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
state
|
||||
}
|
||||
|
||||
is UpdateContextAction -> {
|
||||
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) {
|
||||
state.copy(currentScreen = lastItem)
|
||||
} else {
|
||||
state.copy(systemBackPress = true)
|
||||
}
|
||||
}
|
||||
|
||||
is BottomSheetAction -> {
|
||||
state.copy(
|
||||
bottomSheet = BottomSheetState(
|
||||
action.route,
|
||||
action.globalArgs,
|
||||
action.globalIsBottomSheetOpen
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
else -> state
|
||||
}
|
||||
}
|
||||
|
||||
fun createStore() =
|
||||
createStore(
|
||||
reducer,
|
||||
generateDefaultState(),
|
||||
applyMiddleware(intentNavigationMiddleware)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux
|
||||
|
||||
object ScreenRoutes {
|
||||
const val HOME = "home?tag={tag}"
|
||||
const val SETTINGS = "/settings"
|
||||
const val ADD_ACCOUNT = "addAccount?accountId={accountId}"
|
||||
const val GENERATE_PASSWORD = "/generatePassword"
|
||||
const val ADD_TOTP = "/addTOTP"
|
||||
}
|
||||
|
||||
object BottomSheetRoutes {
|
||||
const val HOME_NAV_MENU = "/home/navMenu"
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
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? = null,
|
||||
val systemBackPress: Boolean = false
|
||||
)
|
||||
|
||||
sealed class ScreenState(val showMainBottomAppBar: Boolean = false)
|
||||
|
||||
data class HomeState(val keyword: String? = null, val tag: String? = null, val sortField: String? = null, val sortAscending: Boolean = true) : ScreenState(true)
|
||||
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,
|
||||
val isBottomSheetOpen: Boolean = false
|
||||
)
|
||||
|
||||
fun generateDefaultState(): KeyPassState {
|
||||
val currentPage = AuthState.Login
|
||||
val bottomSheet = BottomSheetState(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen = false)
|
||||
return KeyPassState(currentScreen = currentPage, bottomSheet = bottomSheet)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.actions
|
||||
|
||||
import android.content.Context
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.ScreenState
|
||||
|
||||
sealed interface Action
|
||||
|
||||
class UpdateContextAction(val context: Context?) : Action
|
||||
|
||||
data class NavigationAction(val state: ScreenState, val clearBackStack: Boolean = false) : Action
|
||||
data class StateUpdateAction(val state: ScreenState) : Action
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.actions
|
||||
|
||||
import android.os.Bundle
|
||||
import com.yogeshpaliyal.keypass.ui.redux.BottomSheetRoutes
|
||||
|
||||
sealed class BottomSheetAction(
|
||||
val route: String,
|
||||
val globalIsBottomSheetOpen: Boolean,
|
||||
val globalArgs: Bundle? = null
|
||||
) : Action {
|
||||
data class HomeNavigationMenu(val isBottomSheetOpen: Boolean, val args: Bundle? = null) :
|
||||
BottomSheetAction(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen, args)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.actions
|
||||
|
||||
sealed interface IntentNavigation : Action {
|
||||
object GeneratePassword : IntentNavigation
|
||||
object BackupActivity : IntentNavigation
|
||||
object ShareApp : IntentNavigation
|
||||
data class AddTOTP(val accountId: String? = null) : IntentNavigation
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.actions
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
|
||||
data class ToastAction(@StringRes val text: Int) : Action
|
||||
data class CopyToClipboard(val password: String) : Action
|
||||
|
||||
object GoBackAction : Action
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.middlewares
|
||||
|
||||
import android.content.Intent
|
||||
import com.yogeshpaliyal.keypass.BuildConfig
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.addTOTP.AddTOTPActivity
|
||||
import com.yogeshpaliyal.keypass.ui.generate.GeneratePasswordActivity
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.KeyPassState
|
||||
import org.reduxkotlin.middleware
|
||||
|
||||
val intentNavigationMiddleware = middleware<KeyPassState> { store, next, action ->
|
||||
val state = store.state
|
||||
|
||||
when (action) {
|
||||
is IntentNavigation.GeneratePassword -> {
|
||||
val intent = Intent(state.context, GeneratePasswordActivity::class.java)
|
||||
state.context?.startActivity(intent)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
data class AccountDetailState(val accountId: Long? = null) : ScreenState()
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
import android.net.Uri
|
||||
import java.net.URLDecoder
|
||||
|
||||
data class BackupScreenState(
|
||||
val isBackupEnabled: Boolean? = null,
|
||||
val isAutoBackupEnabled: Boolean? = null,
|
||||
val overrideAutoBackup: Boolean? = null,
|
||||
val lastBackupTime: Long? = null,
|
||||
val backupDirectory: Uri? = null,
|
||||
val dialog: BackupDialog? = null
|
||||
) : ScreenState() {
|
||||
fun getFormattedBackupDirectory(): String {
|
||||
if (backupDirectory == null) {
|
||||
return ""
|
||||
}
|
||||
val splittedPath = URLDecoder.decode(backupDirectory.toString(), "utf-8").split("/")
|
||||
return splittedPath.get(splittedPath.lastIndex)
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface BackupDialog
|
||||
|
||||
object SelectKeyphraseType : BackupDialog
|
||||
|
||||
object CustomKeyphrase : BackupDialog
|
||||
|
||||
object ShowKeyphrase : BackupDialog
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
import android.os.Bundle
|
||||
|
||||
data class BottomSheetState(
|
||||
val path: String,
|
||||
val args: Bundle? = null,
|
||||
val isBottomSheetOpen: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
data class HomeState(
|
||||
val keyword: String? = null,
|
||||
val tag: String? = null,
|
||||
val sortField: String? = null,
|
||||
val sortAscending: Boolean = true
|
||||
) : ScreenState(true)
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
import android.content.Context
|
||||
import com.yogeshpaliyal.keypass.ui.redux.BottomSheetRoutes
|
||||
|
||||
data class KeyPassState(
|
||||
val context: Context? = null,
|
||||
val currentScreen: ScreenState,
|
||||
val bottomSheet: BottomSheetState? = null,
|
||||
val systemBackPress: Boolean = false
|
||||
)
|
||||
|
||||
fun generateDefaultState(): KeyPassState {
|
||||
val currentPage = AuthState.Login
|
||||
val bottomSheet = BottomSheetState(BottomSheetRoutes.HOME_NAV_MENU, isBottomSheetOpen = false)
|
||||
return KeyPassState(currentScreen = currentPage, bottomSheet = bottomSheet)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
sealed class ScreenState(val showMainBottomAppBar: Boolean = false)
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
object SettingsState : ScreenState(true)
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.yogeshpaliyal.keypass.ui.redux.states
|
||||
|
||||
data class TotpDetailState(val accountId: String? = null) : ScreenState()
|
||||
@@ -1,244 +1,249 @@
|
||||
package com.yogeshpaliyal.keypass.ui.settings
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
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.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
|
||||
import com.yogeshpaliyal.common.utils.email
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
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
|
||||
|
||||
@AndroidEntryPoint
|
||||
class MySettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
@Inject
|
||||
lateinit var appDb: com.yogeshpaliyal.common.AppDatabase
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.preferences, rootKey)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RestoreDialog(
|
||||
selectedFile: Uri,
|
||||
hideDialog: () -> Unit,
|
||||
mViewModel: DashboardViewModel = hiltViewModel()
|
||||
) {
|
||||
val (keyphrase, setKeyPhrase) = remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
val dispatchAction = rememberTypedDispatcher<Action>()
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
) {
|
||||
Box(modifier = Modifier.width(56.dp), Alignment.CenterStart) {
|
||||
if (icon != null) {
|
||||
Icon(painter = rememberVectorPainter(image = icon), contentDescription = "")
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.settings
|
||||
|
||||
import android.net.Uri
|
||||
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.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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.yogeshpaliyal.common.utils.BACKUP_KEY_LENGTH
|
||||
import com.yogeshpaliyal.common.utils.email
|
||||
import com.yogeshpaliyal.keypass.R
|
||||
import com.yogeshpaliyal.keypass.ui.home.DashboardViewModel
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.Action
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.IntentNavigation
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.NavigationAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.actions.ToastAction
|
||||
import com.yogeshpaliyal.keypass.ui.redux.states.BackupScreenState
|
||||
import kotlinx.coroutines.launch
|
||||
import org.reduxkotlin.compose.rememberTypedDispatcher
|
||||
|
||||
@Composable
|
||||
fun RestoreDialog(
|
||||
selectedFile: Uri,
|
||||
hideDialog: () -> Unit,
|
||||
mViewModel: DashboardViewModel = hiltViewModel()
|
||||
) {
|
||||
val (keyphrase, setKeyPhrase) = remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
|
||||
val dispatchAction = rememberTypedDispatcher<Action>()
|
||||
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column {
|
||||
PreferenceItem(title = R.string.security, isCategory = true)
|
||||
PreferenceItem(
|
||||
title = R.string.credentials_backups,
|
||||
summary = R.string.credentials_backups_desc
|
||||
) {
|
||||
dispatchAction(NavigationAction(BackupScreenState()))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PreferenceItem(
|
||||
@StringRes title: Int? = null,
|
||||
@StringRes summary: Int? = null,
|
||||
summaryStr: String? = null,
|
||||
icon: ImageVector? = null,
|
||||
isCategory: Boolean = false,
|
||||
onClickItem: (() -> Unit)? = null
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(1f)
|
||||
.widthIn(48.dp)
|
||||
.padding(horizontal = 16.dp)
|
||||
.clickable(onClickItem != null) {
|
||||
onClickItem?.invoke()
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(modifier = Modifier.width(56.dp), Alignment.CenterStart) {
|
||||
if (icon != null) {
|
||||
Icon(painter = rememberVectorPainter(image = icon), contentDescription = "")
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 16.dp)
|
||||
.fillMaxWidth(1f)
|
||||
) {
|
||||
if (title != null) {
|
||||
if (isCategory) {
|
||||
CategoryTitle(title = title)
|
||||
} else {
|
||||
PreferenceItemTitle(title = title)
|
||||
}
|
||||
}
|
||||
if (summary != null || summaryStr != null) {
|
||||
val summaryText = if (summary != null) {
|
||||
stringResource(id = summary)
|
||||
} else {
|
||||
summaryStr
|
||||
}
|
||||
if (summaryText != null) {
|
||||
Text(text = summaryText, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryTitle(title: Int) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreferenceItemTitle(title: Int) {
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
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()
|
||||
}
|
||||
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)
|
||||
|
||||
@@ -1,20 +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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.yogeshpaliyal.keypass.ui.style
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
|
||||
@Composable
|
||||
fun KeyPassTheme(content: @Composable () -> Unit) {
|
||||
Mdc3Theme {
|
||||
content()
|
||||
}
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.ui.style
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.google.accompanist.themeadapter.material3.Mdc3Theme
|
||||
|
||||
@Composable
|
||||
fun KeyPassTheme(content: @Composable () -> Unit) {
|
||||
Mdc3Theme {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
package com.yogeshpaliyal.keypass.utils
|
||||
|
||||
import android.util.Log
|
||||
import com.yogeshpaliyal.keypass.BuildConfig
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 26-12-2020 20:21
|
||||
*/
|
||||
|
||||
@JvmName("LogHelper")
|
||||
fun Any?.systemOutPrint() {
|
||||
if (BuildConfig.DEBUG) println(this)
|
||||
}
|
||||
|
||||
fun Any?.systemErrPrint() {
|
||||
if (BuildConfig.DEBUG) System.err.println(this)
|
||||
}
|
||||
|
||||
fun Exception?.debugPrintStackTrace() {
|
||||
if (BuildConfig.DEBUG) this?.printStackTrace()
|
||||
}
|
||||
|
||||
fun Throwable?.debugPrintStackTrace() {
|
||||
if (BuildConfig.DEBUG) this?.printStackTrace()
|
||||
}
|
||||
|
||||
fun Any?.logD(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.d(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logE(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.e(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logI(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.i(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logV(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.v(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logW(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.w(tag, this.toString())
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.utils
|
||||
|
||||
import android.util.Log
|
||||
import com.yogeshpaliyal.keypass.BuildConfig
|
||||
|
||||
/*
|
||||
* @author Yogesh Paliyal
|
||||
* techpaliyal@gmail.com
|
||||
* https://techpaliyal.com
|
||||
* created on 26-12-2020 20:21
|
||||
*/
|
||||
|
||||
@JvmName("LogHelper")
|
||||
fun Any?.systemOutPrint() {
|
||||
if (BuildConfig.DEBUG) println(this)
|
||||
}
|
||||
|
||||
fun Any?.systemErrPrint() {
|
||||
if (BuildConfig.DEBUG) System.err.println(this)
|
||||
}
|
||||
|
||||
fun Exception?.debugPrintStackTrace() {
|
||||
if (BuildConfig.DEBUG) this?.printStackTrace()
|
||||
}
|
||||
|
||||
fun Throwable?.debugPrintStackTrace() {
|
||||
if (BuildConfig.DEBUG) this?.printStackTrace()
|
||||
}
|
||||
|
||||
fun Any?.logD(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.d(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logE(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.e(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logI(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.i(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logV(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.v(tag, this.toString())
|
||||
}
|
||||
|
||||
fun Any?.logW(tag: String?) {
|
||||
if (BuildConfig.DEBUG) Log.w(tag, this.toString())
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.yogeshpaliyal.keypass.utils
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
/**
|
||||
* Alias to represent a folder (a String title) into which emails can be placed.
|
||||
*/
|
||||
|
||||
object StringDiffUtil : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
|
||||
}
|
||||
package com.yogeshpaliyal.keypass.utils
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
|
||||
/**
|
||||
* Alias to represent a folder (a String title) into which emails can be placed.
|
||||
*/
|
||||
|
||||
object StringDiffUtil : DiffUtil.ItemCallback<String>() {
|
||||
override fun areItemsTheSame(oldItem: String, newItem: String) = oldItem == newItem
|
||||
override fun areContentsTheSame(oldItem: String, newItem: String) = oldItem == newItem
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout>
|
||||
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragment_container_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:name="com.yogeshpaliyal.keypass.ui.settings.MySettingsFragment" />
|
||||
</layout>
|
||||
@@ -89,5 +89,6 @@
|
||||
<string name="confirm_password">Confirm Password</string>
|
||||
<string name="incorrect_password">Incorrect Password</string>
|
||||
<string name="password_no_match">The password did not match</string>
|
||||
<string name="auto_backup_desc">Your accounts will be backed up whenever account is added or modified</string>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user