simple login screen with simple viewmodel's logic
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
|
import com.android.build.api.dsl.ManagedVirtualDevice
|
||||||
import org.jetbrains.compose.ExperimentalComposeLibrary
|
import org.jetbrains.compose.ExperimentalComposeLibrary
|
||||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
import com.android.build.api.dsl.ManagedVirtualDevice
|
|
||||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
|
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
|
||||||
@@ -55,9 +55,11 @@ kotlin {
|
|||||||
implementation(compose.runtime)
|
implementation(compose.runtime)
|
||||||
implementation(compose.foundation)
|
implementation(compose.foundation)
|
||||||
implementation(compose.material3)
|
implementation(compose.material3)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
implementation(compose.components.resources)
|
implementation(compose.components.resources)
|
||||||
implementation(compose.components.uiToolingPreview)
|
implementation(compose.components.uiToolingPreview)
|
||||||
implementation(libs.voyager.navigator)
|
implementation(libs.voyager.navigator)
|
||||||
|
implementation(libs.voyager.transitions)
|
||||||
implementation(libs.coil)
|
implementation(libs.coil)
|
||||||
implementation(libs.coil.network.ktor)
|
implementation(libs.coil.network.ktor)
|
||||||
implementation(libs.kotlinx.coroutines.core)
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
@@ -68,8 +70,7 @@ kotlin {
|
|||||||
implementation(libs.koin.core)
|
implementation(libs.koin.core)
|
||||||
implementation(libs.koin.compose)
|
implementation(libs.koin.compose)
|
||||||
|
|
||||||
api("dev.icerock.moko:mvvm-compose:0.16.1")
|
implementation("org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.8.0")
|
||||||
api("dev.icerock.moko:mvvm-flow-compose:0.16.1")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
commonTest.dependencies {
|
commonTest.dependencies {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import cafe.adriel.voyager.navigator.Navigator
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import cafe.adriel.voyager.transitions.FadeTransition
|
||||||
import dev.meloda.overseerr.screens.url.presentation.UrlScreen
|
import dev.meloda.overseerr.screens.url.presentation.UrlScreen
|
||||||
import dev.meloda.overseerr.theme.AppTheme
|
import dev.meloda.overseerr.theme.AppTheme
|
||||||
import org.koin.compose.KoinContext
|
import org.koin.compose.KoinContext
|
||||||
@@ -13,7 +14,9 @@ import org.koin.compose.KoinContext
|
|||||||
internal fun App() = KoinContext {
|
internal fun App() = KoinContext {
|
||||||
AppTheme {
|
AppTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
Navigator(UrlScreen())
|
Navigator(UrlScreen()) { navigator ->
|
||||||
|
FadeTransition(navigator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package dev.meloda.overseerr.ext
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
|
||||||
|
fun <T> MutableStateFlow<T>.setValue(function: (T) -> T) {
|
||||||
|
val newValue = function(value)
|
||||||
|
update { newValue }
|
||||||
|
}
|
||||||
+39
-5
@@ -1,9 +1,43 @@
|
|||||||
package dev.meloda.overseerr.screens.login
|
package dev.meloda.overseerr.screens.login
|
||||||
|
|
||||||
import dev.icerock.moko.mvvm.viewmodel.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dev.meloda.overseerr.ext.setValue
|
||||||
|
import dev.meloda.overseerr.screens.login.model.LoginScreenState
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
class LoginViewModel : ViewModel() {
|
interface LoginViewModel {
|
||||||
|
val screenState: StateFlow<LoginScreenState>
|
||||||
|
|
||||||
|
fun onLoginInputChanged(newLogin: String)
|
||||||
|
fun onPasswordInputChanged(newPassword: String)
|
||||||
|
fun onAuthorizeButtonClicked()
|
||||||
|
fun onPasswordVisibilityButtonClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoginViewModelImpl : ViewModel(), LoginViewModel {
|
||||||
|
|
||||||
|
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||||
|
|
||||||
|
override fun onLoginInputChanged(newLogin: String) {
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(login = newLogin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPasswordInputChanged(newPassword: String) {
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(password = newPassword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAuthorizeButtonClicked() {
|
||||||
|
// TODO: 05/08/2024, Danil Nikolaev: add logger
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPasswordVisibilityButtonClicked() {
|
||||||
|
screenState.setValue { old ->
|
||||||
|
old.copy(isPasswordVisible = !old.isPasswordVisible)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1,9 +1,7 @@
|
|||||||
package dev.meloda.overseerr.screens.login.di
|
package dev.meloda.overseerr.screens.login.di
|
||||||
|
|
||||||
import dev.icerock.moko.mvvm.compose.viewModelFactory
|
|
||||||
import dev.meloda.overseerr.screens.login.LoginViewModel
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val loginModule = module {
|
val loginModule = module {
|
||||||
viewModelFactory(::LoginViewModel)
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -1,4 +1,19 @@
|
|||||||
package dev.meloda.overseerr.screens.login.model
|
package dev.meloda.overseerr.screens.login.model
|
||||||
|
|
||||||
class LoginScreenState {
|
data class LoginScreenState(
|
||||||
|
val login: String,
|
||||||
|
val password: String,
|
||||||
|
val isPasswordVisible: Boolean,
|
||||||
|
val isLoginEmptyError: Boolean,
|
||||||
|
val isPasswordEmptyError: Boolean
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val EMPTY: LoginScreenState = LoginScreenState(
|
||||||
|
login = "",
|
||||||
|
password = "",
|
||||||
|
isPasswordVisible = false,
|
||||||
|
isLoginEmptyError = false,
|
||||||
|
isPasswordEmptyError = false
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-12
@@ -1,20 +1,29 @@
|
|||||||
package dev.meloda.overseerr.screens.login.presentation
|
package dev.meloda.overseerr.screens.login.presentation
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowForward
|
||||||
|
import androidx.compose.material.icons.rounded.Visibility
|
||||||
|
import androidx.compose.material.icons.rounded.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import dev.meloda.overseerr.screens.login.LoginViewModel
|
||||||
|
import dev.meloda.overseerr.screens.login.LoginViewModelImpl
|
||||||
|
import dev.meloda.overseerr.screens.login.model.LoginScreenState
|
||||||
|
|
||||||
class LoginScreen : Screen {
|
class LoginScreen : Screen {
|
||||||
|
|
||||||
@@ -22,12 +31,14 @@ class LoginScreen : Screen {
|
|||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val viewModel: LoginViewModel = viewModel { LoginViewModelImpl() }
|
||||||
|
val screenState: LoginScreenState by viewModel.screenState.collectAsState()
|
||||||
|
|
||||||
var loginValue by rememberSaveable {
|
var loginValue by rememberSaveable {
|
||||||
mutableStateOf("")
|
mutableStateOf(screenState.login)
|
||||||
}
|
}
|
||||||
var passwordValue by rememberSaveable {
|
var passwordValue by rememberSaveable {
|
||||||
mutableStateOf("")
|
mutableStateOf(screenState.password)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -54,13 +65,16 @@ class LoginScreen : Screen {
|
|||||||
.padding(padding),
|
.padding(padding),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.fillMaxWidth(0.9f),
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
value = loginValue,
|
value = loginValue,
|
||||||
onValueChange = { newText -> loginValue = newText },
|
onValueChange = { newText ->
|
||||||
placeholder = { Text(text = "Login") }
|
loginValue = newText
|
||||||
|
viewModel.onLoginInputChanged(newText)
|
||||||
|
},
|
||||||
|
placeholder = { Text(text = "Login") },
|
||||||
|
isError = screenState.isLoginEmptyError,
|
||||||
|
singleLine = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -68,8 +82,35 @@ class LoginScreen : Screen {
|
|||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier.fillMaxWidth(0.9f),
|
modifier = Modifier.fillMaxWidth(0.9f),
|
||||||
value = passwordValue,
|
value = passwordValue,
|
||||||
onValueChange = { newText -> passwordValue = newText },
|
onValueChange = { newText ->
|
||||||
placeholder = { Text(text = "Password") }
|
passwordValue = newText
|
||||||
|
viewModel.onPasswordInputChanged(newText)
|
||||||
|
},
|
||||||
|
placeholder = { Text(text = "Password") },
|
||||||
|
isError = screenState.isPasswordEmptyError,
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = viewModel::onPasswordVisibilityButtonClicked) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (screenState.isPasswordVisible) {
|
||||||
|
Icons.Rounded.VisibilityOff
|
||||||
|
} else {
|
||||||
|
Icons.Rounded.Visibility
|
||||||
|
},
|
||||||
|
contentDescription = if (screenState.isPasswordVisible) "Password visible icon"
|
||||||
|
else "Password invisible icon"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions.Default.copy(
|
||||||
|
imeAction = ImeAction.Go,
|
||||||
|
keyboardType = KeyboardType.Password
|
||||||
|
),
|
||||||
|
visualTransformation = if (screenState.isPasswordVisible) {
|
||||||
|
VisualTransformation.None
|
||||||
|
} else {
|
||||||
|
PasswordVisualTransformation()
|
||||||
|
},
|
||||||
|
singleLine = true
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
@@ -80,6 +121,11 @@ class LoginScreen : Screen {
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Text(text = "Authorize")
|
Text(text = "Authorize")
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Rounded.ArrowForward,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package dev.meloda.overseerr.screens.url
|
package dev.meloda.overseerr.screens.url
|
||||||
|
|
||||||
import dev.icerock.moko.mvvm.viewmodel.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class UrlViewModel : ViewModel() {
|
class UrlViewModel : ViewModel() {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
package dev.meloda.overseerr.screens.url.di
|
package dev.meloda.overseerr.screens.url.di
|
||||||
|
|
||||||
import dev.icerock.moko.mvvm.compose.viewModelFactory
|
|
||||||
import dev.meloda.overseerr.screens.url.UrlViewModel
|
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val urlModule = module {
|
val urlModule = module {
|
||||||
viewModelFactory {
|
|
||||||
UrlViewModel()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-2
@@ -2,23 +2,31 @@ package dev.meloda.overseerr.screens.url.presentation
|
|||||||
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
|
import dev.meloda.overseerr.screens.login.presentation.LoginScreen
|
||||||
|
import dev.meloda.overseerr.screens.url.UrlViewModel
|
||||||
|
|
||||||
class UrlScreen : Screen {
|
class UrlScreen : Screen {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
Scaffold(modifier = Modifier.fillMaxSize()) {
|
val viewModel: UrlViewModel = viewModel { UrlViewModel() }
|
||||||
Column(modifier = Modifier.fillMaxSize()) {
|
|
||||||
|
Scaffold(modifier = Modifier.fillMaxSize()) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
) {
|
||||||
Text(text = "Input url screen")
|
Text(text = "Input url screen")
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ androidx-activityCompose = { module = "androidx.activity:activity-compose", vers
|
|||||||
androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" }
|
androidx-uitest-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" }
|
||||||
androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" }
|
androidx-uitest-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" }
|
||||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
|
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||||
coil = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" }
|
coil = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil" }
|
||||||
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
||||||
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
|
||||||
|
|||||||
Reference in New Issue
Block a user