Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Unlock screen with MVI architecture #55

Merged
merged 2 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,11 @@ dependencies {
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.5.2")
testImplementation("io.kotest:kotest-assertions-core-jvm:5.5.3")
testImplementation("io.mockk:mockk:1.12.3")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")

// Kotlin
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.20")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.7.3")

// Compose
implementation(compose.desktop.currentOs)
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/github/ai/autokpass/Main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.github.ai.autokpass.di.GlobalInjector.get
import com.github.ai.autokpass.di.KoinModule
import com.github.ai.autokpass.domain.StartInteractor
import com.github.ai.autokpass.model.ParsedConfig
import com.github.ai.autokpass.presentation.ui.Screen
import com.github.ai.autokpass.presentation.ui.core.strings.StringResources
import com.github.ai.autokpass.presentation.ui.root.RootComponent
import com.github.ai.autokpass.presentation.ui.root.RootScreen
Expand All @@ -32,7 +33,7 @@ fun main(args: Array<String>) {
val lifecycle = LifecycleRegistry()
val rootComponent = RootComponent(
componentContext = DefaultComponentContext(lifecycle),
startScreen = interactor.determineStartScreen(configResult),
startScreen = Screen.Unlock,
appArguments = arguments
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import com.github.ai.autokpass.model.RawConfig
import com.github.ai.autokpass.model.Result
import com.github.ai.autokpass.presentation.ui.core.strings.StringResources
import java.io.ByteArrayInputStream
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow

class ConfigRepository(
Expand All @@ -22,41 +21,57 @@ class ConfigRepository(
private val strings: StringResources
) {

private val current = MutableStateFlow<Result<ParsedConfig>>(
Result.Error(EmptyConfigException(strings))
)
private val currentConfig = MutableStateFlow<ConfigState>(ConfigState.Empty)

fun initialize(commandLineArguments: Array<String>): Result<ParsedConfig> {
val configResult = CommandLineConfigReader(
val readCommandLineValuesResult = CommandLineConfigReader(
commandLineArguments,
strings
).readConfig()

if (configResult.isFailed()) {
return configResult.asErrorOrThrow()
if (readCommandLineValuesResult.isFailed()) {
return readCommandLineValuesResult.asErrorOrThrow()
}

val fileConfigResult = readConfigFromFile()
if (fileConfigResult.isFailed()) {
return fileConfigResult.asErrorOrThrow()
val readFileValuesResult = readConfigFromFile()
if (readFileValuesResult.isFailed()) {
return readFileValuesResult.asErrorOrThrow()
}

val config = configResult.getDataOrThrow()
val fileConfig = fileConfigResult.getDataOrThrow()
val commandLineValues = readCommandLineValuesResult.getDataOrThrow()
val fileValues = readFileValuesResult.getDataOrThrow()

val result = when {
fileConfig != null && config.isEmpty() -> configParser.validateAndParse(fileConfig)
!config.isEmpty() -> configParser.validateAndParse(config)
else -> Result.Error(EmptyConfigException(strings))
val config = when {
fileValues != null && commandLineValues.isEmpty() -> {
ConfigState.FileConfig(
config = configParser.validateAndParse(fileValues)
)
}

!commandLineValues.isEmpty() -> {
ConfigState.CommandLineConfig(
config = configParser.validateAndParse(commandLineValues)
)
}

else -> ConfigState.Empty
}

current.value = result
currentConfig.value = config

return result
return when (config) {
is ConfigState.CommandLineConfig -> config.config
is ConfigState.FileConfig -> config.config
else -> Result.Error(EmptyConfigException(strings))
}
}

fun load(): Flow<Result<ParsedConfig>> {
return current
fun getCurrent(): Result<ParsedConfig> {
return when (val config = currentConfig.value) {
is ConfigState.CommandLineConfig -> config.config
is ConfigState.FileConfig -> config.config
else -> Result.Error(EmptyConfigException(strings))
}
}

private fun readConfigFromFile(): Result<RawConfig?> {
Expand Down Expand Up @@ -101,6 +116,19 @@ class ConfigRepository(
}
}

private sealed class ConfigState {

object Empty : ConfigState()

data class CommandLineConfig(
val config: Result<ParsedConfig>
) : ConfigState()

data class FileConfig(
val config: Result<ParsedConfig>
) : ConfigState()
}

companion object {
private const val ENVIRONMENT_USER_HOME = "user.home"
private const val CONFIG_FILE_PATH = ".config/autokpass/autokpass.cfg"
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/com/github/ai/autokpass/di/KoinModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import com.github.ai.autokpass.presentation.ui.screens.selectPattern.SelectPatte
import com.github.ai.autokpass.presentation.ui.screens.selectPattern.SelectPatternInteractor
import com.github.ai.autokpass.presentation.ui.screens.selectPattern.SelectPatternViewModel
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockInteractor
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockInteractorImpl
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel
import org.koin.dsl.module
import org.slf4j.Logger
Expand Down Expand Up @@ -86,19 +87,18 @@ object KoinModule {

// interactors
single { StartInteractor(get(), get()) }
single { UnlockInteractor(get(), get(), get()) }
single<UnlockInteractor> { UnlockInteractorImpl(get(), get(), get()) }
single { SelectEntryInteractor(get(), get(), get(), get()) }
single { SelectPatternInteractor(get(), get(), get(), get()) }
single { AutotypeInteractor(get(), get(), get(), get(), get(), get(), get()) }

// View Models
factory { (router: Router, appArgs: ParsedConfig) ->
factory { (router: Router) ->
UnlockViewModel(
get(),
get(),
get(),
router,
appArgs
router
)
}
factory { (router: Router, args: SelectEntryArgs, appArgs: ParsedConfig) ->
Expand Down
21 changes: 0 additions & 21 deletions src/main/java/com/github/ai/autokpass/domain/StartInteractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@ package com.github.ai.autokpass.domain

import com.github.ai.autokpass.data.config.ConfigRepository
import com.github.ai.autokpass.domain.usecases.PrintGreetingsUseCase
import com.github.ai.autokpass.model.KeepassKey
import com.github.ai.autokpass.model.ParsedConfig
import com.github.ai.autokpass.model.Result
import com.github.ai.autokpass.presentation.ui.Screen
import com.github.ai.autokpass.presentation.ui.screens.selectEntry.SelectEntryArgs
import java.io.File

class StartInteractor(
private val configRepository: ConfigRepository,
Expand All @@ -19,21 +15,4 @@ class StartInteractor(

return configRepository.initialize(commandLineArguments)
}

fun determineStartScreen(configResult: Result<ParsedConfig>): Screen {
val config = configResult.getDataOrNull()

return when {
configResult.isFailed() || config?.keyPath == null -> {
Screen.Unlock
}
else -> {
val key = KeepassKey.FileKey(
file = File(config.keyPath),
processingCommand = config.keyProcessingCommand
)
Screen.SelectEntry(SelectEntryArgs(key))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
package com.github.ai.autokpass.presentation.ui.core.navigation

import com.arkivanov.decompose.router.stack.push
import com.github.ai.autokpass.presentation.ui.Screen
import com.github.ai.autokpass.presentation.ui.root.RootComponent
import kotlin.system.exitProcess

class Router(private val rootComponent: RootComponent) {

fun navigateTo(screen: Screen) {
rootComponent.navigation.push(screen)
}

fun exit() {
exitProcess(0)
}
interface Router {
fun navigateTo(screen: Screen)
fun exit()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.github.ai.autokpass.presentation.ui.core.navigation

import com.arkivanov.decompose.router.stack.push
import com.github.ai.autokpass.presentation.ui.Screen
import com.github.ai.autokpass.presentation.ui.root.RootComponent
import kotlin.system.exitProcess

class RouterImpl(private val rootComponent: RootComponent) : Router {

override fun navigateTo(screen: Screen) {
rootComponent.navigation.push(screen)
}

override fun exit() {
exitProcess(0)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.arkivanov.decompose.router.stack.childStack
import com.github.ai.autokpass.model.ParsedConfig
import com.github.ai.autokpass.presentation.ui.Screen
import com.github.ai.autokpass.presentation.ui.core.navigation.Router
import com.github.ai.autokpass.presentation.ui.core.navigation.RouterImpl
import com.github.ai.autokpass.presentation.ui.screens.autotype.AutotypeComponent
import com.github.ai.autokpass.presentation.ui.screens.selectEntry.SelectEntryComponent
import com.github.ai.autokpass.presentation.ui.screens.selectPattern.SelectPatternComponent
Expand All @@ -18,7 +19,7 @@ class RootComponent(
) : ComponentContext by componentContext {

val navigation = StackNavigation<Screen>()
val router = Router(this)
val router: Router = RouterImpl(this)
val viewModel = RootViewModel()
val childStack = childStack(
source = navigation,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
package com.github.ai.autokpass.presentation.ui.screens.unlock

import com.github.ai.autokpass.data.config.ConfigRepository
import com.github.ai.autokpass.domain.coroutine.Dispatchers
import com.github.ai.autokpass.domain.usecases.ReadDatabaseUseCase
import com.github.ai.autokpass.model.KeepassKey
import com.github.ai.autokpass.model.ParsedConfig
import com.github.ai.autokpass.model.Result
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext

class UnlockInteractor(
private val configRepository: ConfigRepository,
private val dispatchers: Dispatchers,
private val readDatabaseUseCase: ReadDatabaseUseCase
) {

fun loadConfig(): Flow<Result<ParsedConfig>> {
return configRepository.load()
}
interface UnlockInteractor {
suspend fun loadConfig(): Result<ParsedConfig>

suspend fun unlockDatabase(
password: String,
key: KeepassKey,
filePath: String
): Result<Unit> {
return withContext(dispatchers.IO) {
readDatabaseUseCase.readDatabase(
key = KeepassKey.PasswordKey(password),
filePath = filePath
)
.mapWith(Unit)
}
}
): Result<Unit>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.github.ai.autokpass.presentation.ui.screens.unlock

import com.github.ai.autokpass.data.config.ConfigRepository
import com.github.ai.autokpass.domain.coroutine.Dispatchers
import com.github.ai.autokpass.domain.usecases.ReadDatabaseUseCase
import com.github.ai.autokpass.model.KeepassKey
import com.github.ai.autokpass.model.ParsedConfig
import com.github.ai.autokpass.model.Result
import kotlinx.coroutines.withContext

class UnlockInteractorImpl(
private val configRepository: ConfigRepository,
private val dispatchers: Dispatchers,
private val readDatabaseUseCase: ReadDatabaseUseCase
) : UnlockInteractor {

override suspend fun loadConfig(): Result<ParsedConfig> =
withContext(dispatchers.IO) {
configRepository.getCurrent()
}

override suspend fun unlockDatabase(
key: KeepassKey,
filePath: String
): Result<Unit> {
return withContext(dispatchers.IO) {
readDatabaseUseCase.readDatabase(
key = key,
filePath = filePath
)
.mapWith(Unit)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ import com.github.ai.autokpass.presentation.ui.core.TopBar
import com.github.ai.autokpass.presentation.ui.core.strings.StringResources
import com.github.ai.autokpass.presentation.ui.core.strings.StringResourcesImpl
import com.github.ai.autokpass.presentation.ui.core.theme.AppTextStyles
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel.ScreenState
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel.UnlockIntent.OnErrorIconClicked
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel.UnlockIntent.OnPasswordInputChanged
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel.UnlockIntent.OnPasswordVisibilityChanged
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel.UnlockIntent.OnUnlockButtonClicked
import com.github.ai.autokpass.presentation.ui.screens.unlock.UnlockViewModel.UnlockState
import com.github.ai.autokpass.util.StringUtils.EMPTY

@Composable
Expand All @@ -49,19 +53,27 @@ fun UnlockScreen(viewModel: UnlockViewModel) {
Box(modifier = Modifier.fillMaxSize()) {
with(state) {
when (this) {
is ScreenState.Loading -> {
is UnlockState.Loading -> {
CenteredBox { ProgressBar() }
}
is ScreenState.Data -> {
is UnlockState.Data -> {
ScreenContent(
state = this,
onInputTextChanged = { text -> viewModel.onPasswordInputChanged(text) },
onUnlockButtonClicked = { viewModel.unlockDatabase() },
onPasswordIconClicked = { viewModel.togglePasswordVisibility() },
onErrorIconClicked = { viewModel.clearError() }
onInputTextChanged = { text ->
viewModel.sendIntent(OnPasswordInputChanged(text))
},
onUnlockButtonClicked = {
viewModel.sendIntent(OnUnlockButtonClicked)
},
onPasswordIconClicked = {
viewModel.sendIntent(OnPasswordVisibilityChanged)
},
onErrorIconClicked = {
viewModel.sendIntent(OnErrorIconClicked)
}
)
}
is ScreenState.Error -> {
is UnlockState.Error -> {
CenteredBox {
ErrorStateView(message = message)
}
Expand All @@ -75,7 +87,7 @@ fun UnlockScreen(viewModel: UnlockViewModel) {
@OptIn(ExperimentalComposeUiApi::class)
@Composable
private fun ScreenContent(
state: ScreenState.Data,
state: UnlockState.Data,
onInputTextChanged: (text: String) -> Unit,
onUnlockButtonClicked: () -> Unit,
onPasswordIconClicked: () -> Unit,
Expand Down
Loading