improve login screen UI and logic & fixes for blur
This commit is contained in:
@@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarDefaults
|
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -26,6 +25,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
@@ -42,6 +42,7 @@ import dev.meloda.fast.model.BaseError
|
|||||||
import dev.meloda.fast.model.BottomNavigationItem
|
import dev.meloda.fast.model.BottomNavigationItem
|
||||||
import dev.meloda.fast.navigation.MainGraph
|
import dev.meloda.fast.navigation.MainGraph
|
||||||
import dev.meloda.fast.profile.navigation.profileScreen
|
import dev.meloda.fast.profile.navigation.profileScreen
|
||||||
|
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import dev.meloda.fast.ui.theme.LocalUser
|
import dev.meloda.fast.ui.theme.LocalUser
|
||||||
@@ -58,7 +59,7 @@ fun MainScreen(
|
|||||||
onMessageClicked: (userId: Int) -> Unit = {},
|
onMessageClicked: (userId: Int) -> Unit = {},
|
||||||
onCreateChatClicked: () -> Unit = {}
|
onCreateChatClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val currentTheme = LocalThemeConfig.current
|
val theme = LocalThemeConfig.current
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
@@ -75,18 +76,12 @@ fun MainScreen(
|
|||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.then(
|
.fillMaxWidth()
|
||||||
if (currentTheme.enableBlur) {
|
.hazeEffect(
|
||||||
Modifier.hazeEffect(
|
|
||||||
state = hazeState,
|
state = hazeState,
|
||||||
style = HazeMaterials.thick()
|
style = HazeMaterials.thick()
|
||||||
)
|
),
|
||||||
} else Modifier
|
containerColor = Color.Transparent
|
||||||
)
|
|
||||||
.fillMaxWidth(),
|
|
||||||
containerColor = NavigationBarDefaults.containerColor.copy(
|
|
||||||
alpha = if (currentTheme.enableBlur) 0f else 1f
|
|
||||||
)
|
|
||||||
) {
|
) {
|
||||||
navigationItems.forEachIndexed { index, item ->
|
navigationItems.forEachIndexed { index, item ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
@@ -145,11 +140,10 @@ fun MainScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(bottom = padding.calculateBottomPadding())
|
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalHazeState provides hazeState,
|
LocalHazeState provides hazeState,
|
||||||
// LocalBottomPadding provides if (currentTheme.enableBlur) padding.calculateBottomPadding() else 0.dp
|
LocalBottomPadding provides padding.calculateBottomPadding()
|
||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package dev.meloda.fast.ui.basic
|
|
||||||
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.autofill.AutofillManager
|
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.autofill.Autofill
|
|
||||||
import androidx.compose.ui.autofill.AutofillNode
|
|
||||||
import androidx.compose.ui.autofill.AutofillType
|
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
|
||||||
import androidx.compose.ui.geometry.Rect
|
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.platform.LocalAutofill
|
|
||||||
import androidx.compose.ui.platform.LocalAutofillTree
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalView
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
fun Modifier.connectNode(handler: AutoFillHandler): Modifier {
|
|
||||||
return with(handler) { fillBounds() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Modifier.defaultFocusChangeAutoFill(handler: AutoFillHandler): Modifier {
|
|
||||||
return this.then(
|
|
||||||
Modifier.onFocusChanged {
|
|
||||||
if (it.isFocused) {
|
|
||||||
handler.request()
|
|
||||||
} else {
|
|
||||||
handler.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
|
||||||
fun autoFillRequestHandler(
|
|
||||||
autofillTypes: List<AutofillType> = listOf(),
|
|
||||||
onFill: (String) -> Unit,
|
|
||||||
): AutoFillHandler {
|
|
||||||
val view = LocalView.current
|
|
||||||
val context = LocalContext.current
|
|
||||||
var isFillRecently = remember { false }
|
|
||||||
val autoFillNode = remember {
|
|
||||||
AutofillNode(
|
|
||||||
autofillTypes = autofillTypes,
|
|
||||||
onFill = {
|
|
||||||
isFillRecently = true
|
|
||||||
onFill(it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val autofill = LocalAutofill.current
|
|
||||||
LocalAutofillTree.current += autoFillNode
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return EmptyAutoFillHandler
|
|
||||||
|
|
||||||
return remember {
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
|
||||||
object : AutoFillHandler {
|
|
||||||
val autofillManager = context.getSystemService(AutofillManager::class.java)
|
|
||||||
override fun requestManual() {
|
|
||||||
autofillManager.requestAutofill(
|
|
||||||
view,
|
|
||||||
autoFillNode.id,
|
|
||||||
autoFillNode.boundingBox?.toAndroidRect() ?: error("BoundingBox is not provided yet")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun requestVerifyManual() {
|
|
||||||
if (isFillRecently) {
|
|
||||||
isFillRecently = false
|
|
||||||
requestManual()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val autoFill: Autofill?
|
|
||||||
get() = autofill
|
|
||||||
|
|
||||||
override val autoFillNode: AutofillNode
|
|
||||||
get() = autoFillNode
|
|
||||||
|
|
||||||
override fun request() {
|
|
||||||
autofill?.requestAutofillForNode(autofillNode = autoFillNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cancel() {
|
|
||||||
autofill?.cancelAutofillForNode(autofillNode = autoFillNode)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun Modifier.fillBounds(): Modifier {
|
|
||||||
return this.then(
|
|
||||||
Modifier.onGloballyPositioned {
|
|
||||||
autoFillNode.boundingBox = it.boundsInWindow()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun Rect.toAndroidRect(): android.graphics.Rect {
|
|
||||||
return android.graphics.Rect(
|
|
||||||
left.roundToInt(),
|
|
||||||
top.roundToInt(),
|
|
||||||
right.roundToInt(),
|
|
||||||
bottom.roundToInt()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
interface AutoFillHandler {
|
|
||||||
|
|
||||||
val autoFill: Autofill?
|
|
||||||
val autoFillNode: AutofillNode?
|
|
||||||
fun requestVerifyManual()
|
|
||||||
fun requestManual()
|
|
||||||
fun request()
|
|
||||||
fun cancel()
|
|
||||||
fun Modifier.fillBounds(): Modifier
|
|
||||||
}
|
|
||||||
|
|
||||||
@ExperimentalComposeUiApi
|
|
||||||
data object EmptyAutoFillHandler : AutoFillHandler {
|
|
||||||
override val autoFill: Autofill? = null
|
|
||||||
override val autoFillNode: AutofillNode? = null
|
|
||||||
override fun requestVerifyManual() {}
|
|
||||||
override fun requestManual() {}
|
|
||||||
override fun request() {}
|
|
||||||
override fun cancel() {}
|
|
||||||
override fun Modifier.fillBounds(): Modifier = this.then(Modifier)
|
|
||||||
}
|
|
||||||
@@ -6,19 +6,26 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ErrorView(
|
fun ErrorView(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
iconResId: Int? = UiR.drawable.round_error_24,
|
||||||
text: String,
|
text: String,
|
||||||
buttonText: String? = null,
|
buttonText: String? = null,
|
||||||
onButtonClick: (() -> Unit)? = null,
|
onButtonClick: (() -> Unit)? = null,
|
||||||
@@ -30,6 +37,16 @@ fun ErrorView(
|
|||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
|
iconResId?.let {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(120.dp),
|
||||||
|
painter = painterResource(iconResId),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
@@ -37,9 +54,10 @@ fun ErrorView(
|
|||||||
)
|
)
|
||||||
|
|
||||||
buttonText?.let {
|
buttonText?.let {
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Button(
|
Button(
|
||||||
onClick = { onButtonClick?.invoke() }
|
onClick = { onButtonClick?.invoke() },
|
||||||
|
shape = RoundedCornerShape(6.dp)
|
||||||
) {
|
) {
|
||||||
Text(text = buttonText)
|
Text(text = buttonText)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,13c-0.55,0 -1,-0.45 -1,-1L11,8c0,-0.55 0.45,-1 1,-1s1,0.45 1,1v4c0,0.55 -0.45,1 -1,1zM13,17h-2v-2h2v2z" />
|
||||||
|
|
||||||
|
</vector>
|
||||||
@@ -46,6 +46,13 @@ androidComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: 29-Mar-25, Danil Nikolaev: remove when autofill changes will be in release
|
||||||
|
configurations.all {
|
||||||
|
resolutionStrategy {
|
||||||
|
force(libs.compose.ui)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "dev.meloda.fast.auth"
|
namespace = "dev.meloda.fast.auth"
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ import androidx.navigation.navigation
|
|||||||
import dev.meloda.fast.auth.captcha.navigation.captchaScreen
|
import dev.meloda.fast.auth.captcha.navigation.captchaScreen
|
||||||
import dev.meloda.fast.auth.captcha.navigation.navigateToCaptcha
|
import dev.meloda.fast.auth.captcha.navigation.navigateToCaptcha
|
||||||
import dev.meloda.fast.auth.captcha.navigation.setCaptchaResult
|
import dev.meloda.fast.auth.captcha.navigation.setCaptchaResult
|
||||||
import dev.meloda.fast.auth.login.navigation.Logo
|
import dev.meloda.fast.auth.login.navigation.Login
|
||||||
import dev.meloda.fast.auth.login.navigation.loginScreen
|
import dev.meloda.fast.auth.login.navigation.loginScreen
|
||||||
import dev.meloda.fast.auth.login.navigation.navigateToLogin
|
|
||||||
import dev.meloda.fast.auth.userbanned.model.UserBannedArguments
|
import dev.meloda.fast.auth.userbanned.model.UserBannedArguments
|
||||||
import dev.meloda.fast.auth.userbanned.navigation.navigateToUserBanned
|
import dev.meloda.fast.auth.userbanned.navigation.navigateToUserBanned
|
||||||
import dev.meloda.fast.auth.userbanned.navigation.userBannedRoute
|
import dev.meloda.fast.auth.userbanned.navigation.userBannedRoute
|
||||||
@@ -26,9 +25,7 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
navigation<AuthGraph>(
|
navigation<AuthGraph>(startDestination = Login) {
|
||||||
startDestination = Logo
|
|
||||||
) {
|
|
||||||
loginScreen(
|
loginScreen(
|
||||||
onNavigateToCaptcha = { arguments ->
|
onNavigateToCaptcha = { arguments ->
|
||||||
navController.navigateToCaptcha(
|
navController.navigateToCaptcha(
|
||||||
@@ -57,7 +54,6 @@ fun NavGraphBuilder.authNavGraph(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onNavigateToCredentials = navController::navigateToLogin,
|
|
||||||
navController = navController
|
navController = navController
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package dev.meloda.fast.auth.login
|
package dev.meloda.fast.auth.login
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginError
|
import dev.meloda.fast.auth.login.model.LoginDialog
|
||||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
@@ -36,7 +37,7 @@ import kotlinx.coroutines.launch
|
|||||||
|
|
||||||
interface LoginViewModel {
|
interface LoginViewModel {
|
||||||
val screenState: StateFlow<LoginScreenState>
|
val screenState: StateFlow<LoginScreenState>
|
||||||
val loginError: StateFlow<LoginError?>
|
val loginDialog: StateFlow<LoginDialog?>
|
||||||
|
|
||||||
val validationCode: StateFlow<String?>
|
val validationCode: StateFlow<String?>
|
||||||
val validationArguments: StateFlow<LoginValidationArguments?>
|
val validationArguments: StateFlow<LoginValidationArguments?>
|
||||||
@@ -44,7 +45,11 @@ interface LoginViewModel {
|
|||||||
val captchaArguments: StateFlow<CaptchaArguments?>
|
val captchaArguments: StateFlow<CaptchaArguments?>
|
||||||
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
val userBannedArguments: StateFlow<LoginUserBannedArguments?>
|
||||||
val isNeedToOpenMain: StateFlow<Boolean>
|
val isNeedToOpenMain: StateFlow<Boolean>
|
||||||
val isNeedToShowFastSignInAlert: StateFlow<Boolean>
|
|
||||||
|
fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle)
|
||||||
|
fun onDialogDismissed(dialog: LoginDialog)
|
||||||
|
|
||||||
|
fun onBackPressed()
|
||||||
|
|
||||||
fun onPasswordVisibilityButtonClicked()
|
fun onPasswordVisibilityButtonClicked()
|
||||||
|
|
||||||
@@ -53,8 +58,6 @@ interface LoginViewModel {
|
|||||||
|
|
||||||
fun onSignInButtonClicked()
|
fun onSignInButtonClicked()
|
||||||
|
|
||||||
fun onErrorDialogDismissed()
|
|
||||||
|
|
||||||
fun onNavigatedToMain()
|
fun onNavigatedToMain()
|
||||||
fun onNavigatedToUserBanned()
|
fun onNavigatedToUserBanned()
|
||||||
fun onNavigatedToCaptcha()
|
fun onNavigatedToCaptcha()
|
||||||
@@ -64,9 +67,6 @@ interface LoginViewModel {
|
|||||||
fun onCaptchaCodeReceived(code: String)
|
fun onCaptchaCodeReceived(code: String)
|
||||||
|
|
||||||
fun onLogoLongClicked()
|
fun onLogoLongClicked()
|
||||||
|
|
||||||
fun onFastLogInAlertDismissed()
|
|
||||||
fun onFastLogInAlertConfirmClicked(token: String)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LoginViewModelImpl(
|
class LoginViewModelImpl(
|
||||||
@@ -78,7 +78,7 @@ class LoginViewModelImpl(
|
|||||||
) : ViewModel(), LoginViewModel {
|
) : ViewModel(), LoginViewModel {
|
||||||
|
|
||||||
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
override val screenState = MutableStateFlow(LoginScreenState.EMPTY)
|
||||||
override val loginError = MutableStateFlow<LoginError?>(null)
|
override val loginDialog = MutableStateFlow<LoginDialog?>(null)
|
||||||
|
|
||||||
override val validationCode = MutableStateFlow<String?>(null)
|
override val validationCode = MutableStateFlow<String?>(null)
|
||||||
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
override val validationArguments = MutableStateFlow<LoginValidationArguments?>(null)
|
||||||
@@ -86,39 +86,63 @@ class LoginViewModelImpl(
|
|||||||
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
override val captchaArguments = MutableStateFlow<CaptchaArguments?>(null)
|
||||||
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
override val userBannedArguments = MutableStateFlow<LoginUserBannedArguments?>(null)
|
||||||
override val isNeedToOpenMain = MutableStateFlow(false)
|
override val isNeedToOpenMain = MutableStateFlow(false)
|
||||||
override val isNeedToShowFastSignInAlert = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private val validationState: StateFlow<List<LoginValidationResult>> =
|
private val validationState: StateFlow<List<LoginValidationResult>> =
|
||||||
screenState.map(loginValidator::validate)
|
screenState.map(loginValidator::validate)
|
||||||
.stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty))
|
.stateIn(viewModelScope, SharingStarted.Eagerly, listOf(LoginValidationResult.Empty))
|
||||||
|
|
||||||
|
override fun onDialogConfirmed(dialog: LoginDialog, bundle: Bundle) {
|
||||||
|
onDialogDismissed(dialog)
|
||||||
|
|
||||||
|
when (dialog) {
|
||||||
|
is LoginDialog.Error -> Unit
|
||||||
|
|
||||||
|
LoginDialog.FastAuth -> {
|
||||||
|
val token = bundle.getString("token")?.trim() ?: return
|
||||||
|
fastAuth(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDialogDismissed(dialog: LoginDialog) {
|
||||||
|
loginDialog.setValue { null }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed() {
|
||||||
|
screenState.setValue { old -> old.copy(showLogo = true) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onPasswordVisibilityButtonClicked() {
|
override fun onPasswordVisibilityButtonClicked() {
|
||||||
screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
|
screenState.setValue { old -> old.copy(passwordVisible = !old.passwordVisible) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLoginInputChanged(newLogin: String) {
|
override fun onLoginInputChanged(newLogin: String) {
|
||||||
val newState = screenState.value.copy(
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
login = newLogin.trim(),
|
login = newLogin.trim(),
|
||||||
loginError = false
|
loginError = false
|
||||||
)
|
)
|
||||||
screenState.setValue { newState }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPasswordInputChanged(newPassword: String) {
|
override fun onPasswordInputChanged(newPassword: String) {
|
||||||
val newState = screenState.value.copy(
|
screenState.setValue { old ->
|
||||||
|
old.copy(
|
||||||
password = newPassword.trim(),
|
password = newPassword.trim(),
|
||||||
passwordError = false
|
passwordError = false
|
||||||
)
|
)
|
||||||
screenState.setValue { newState }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSignInButtonClicked() {
|
override fun onSignInButtonClicked() {
|
||||||
if (screenState.value.isLoading) return
|
if (screenState.value.isLoading) return
|
||||||
login()
|
|
||||||
|
if (screenState.value.showLogo) {
|
||||||
|
screenState.setValue { old -> old.copy(showLogo = false) }
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onErrorDialogDismissed() {
|
login()
|
||||||
loginError.update { null }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigatedToMain() {
|
override fun onNavigatedToMain() {
|
||||||
@@ -150,14 +174,10 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onLogoLongClicked() {
|
override fun onLogoLongClicked() {
|
||||||
isNeedToShowFastSignInAlert.update { true }
|
loginDialog.setValue { LoginDialog.FastAuth }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFastLogInAlertDismissed() {
|
private fun fastAuth(token: String) {
|
||||||
isNeedToShowFastSignInAlert.update { false }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFastLogInAlertConfirmClicked(token: String) {
|
|
||||||
var currentAccount = AccountEntity(
|
var currentAccount = AccountEntity(
|
||||||
userId = -1,
|
userId = -1,
|
||||||
accessToken = token,
|
accessToken = token,
|
||||||
@@ -177,12 +197,12 @@ class LoginViewModelImpl(
|
|||||||
nomCase = null
|
nomCase = null
|
||||||
).listenValue(viewModelScope) { state ->
|
).listenValue(viewModelScope) { state ->
|
||||||
state.processState(
|
state.processState(
|
||||||
error = { error ->
|
error = {
|
||||||
UserConfig.currentUserId = -1
|
UserConfig.currentUserId = -1
|
||||||
UserConfig.userId = -1
|
UserConfig.userId = -1
|
||||||
UserConfig.accessToken = ""
|
UserConfig.accessToken = ""
|
||||||
|
|
||||||
// TODO: 19/07/2024, Danil Nikolaev: show error?
|
loginDialog.setValue { LoginDialog.Error() }
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { response ->
|
||||||
val actualUserId = requireNotNull(response).id
|
val actualUserId = requireNotNull(response).id
|
||||||
@@ -241,7 +261,7 @@ class LoginViewModelImpl(
|
|||||||
val accessToken = response.accessToken
|
val accessToken = response.accessToken
|
||||||
|
|
||||||
if (userId == null || accessToken == null) {
|
if (userId == null || accessToken == null) {
|
||||||
loginError.update { LoginError.Unknown }
|
loginDialog.setValue { LoginDialog.Error() }
|
||||||
return@processState
|
return@processState
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,7 +332,9 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.InvalidCredentialsError -> {
|
OAuthErrorDomain.InvalidCredentialsError -> {
|
||||||
loginError.update { LoginError.WrongCredentials }
|
loginDialog.setValue {
|
||||||
|
LoginDialog.Error(errorText = "Wrong login or password.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is OAuthErrorDomain.UserBannedError -> {
|
is OAuthErrorDomain.UserBannedError -> {
|
||||||
@@ -326,19 +348,25 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.WrongValidationCode -> {
|
OAuthErrorDomain.WrongValidationCode -> {
|
||||||
loginError.update { LoginError.WrongValidationCode }
|
loginDialog.setValue {
|
||||||
|
LoginDialog.Error(errorText = "Wrong validation code.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
OAuthErrorDomain.WrongValidationCodeFormat -> {
|
||||||
loginError.update { LoginError.WrongValidationCodeFormat }
|
loginDialog.setValue {
|
||||||
|
LoginDialog.Error(errorText = "Wrong validation code format.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.TooManyTriesError -> {
|
OAuthErrorDomain.TooManyTriesError -> {
|
||||||
loginError.update { LoginError.TooManyTries }
|
loginDialog.setValue {
|
||||||
|
LoginDialog.Error(errorText = "Too many tries. Try in another hour or later.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuthErrorDomain.UnknownError -> {
|
OAuthErrorDomain.UnknownError -> {
|
||||||
loginError.update { LoginError.Unknown }
|
loginDialog.setValue { LoginDialog.Error() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,9 +374,9 @@ class LoginViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
is State.Error.TestError -> {
|
is State.Error.TestError -> {
|
||||||
val message = stateError.message
|
loginDialog.setValue {
|
||||||
val error = LoginError.SimpleError(message = message)
|
LoginDialog.Error(errorText = stateError.message)
|
||||||
loginError.update { error }
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package dev.meloda.fast.auth.login.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
sealed class LoginDialog {
|
||||||
|
|
||||||
|
data object FastAuth : LoginDialog()
|
||||||
|
|
||||||
|
data class Error(
|
||||||
|
val errorText: String? = null,
|
||||||
|
val errorTextResId: Int? = null
|
||||||
|
) : LoginDialog()
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import androidx.compose.runtime.Immutable
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class LoginScreenState(
|
data class LoginScreenState(
|
||||||
|
val showLogo: Boolean,
|
||||||
val login: String,
|
val login: String,
|
||||||
val password: String,
|
val password: String,
|
||||||
val isLoading: Boolean,
|
val isLoading: Boolean,
|
||||||
@@ -14,6 +15,7 @@ data class LoginScreenState(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val EMPTY = LoginScreenState(
|
val EMPTY = LoginScreenState(
|
||||||
|
showLogo = true,
|
||||||
login = "",
|
login = "",
|
||||||
password = "",
|
password = "",
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
|||||||
@@ -10,22 +10,17 @@ import dev.meloda.fast.auth.login.model.CaptchaArguments
|
|||||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
import dev.meloda.fast.auth.login.presentation.LoginRoute
|
import dev.meloda.fast.auth.login.presentation.LoginRoute
|
||||||
import dev.meloda.fast.auth.login.presentation.LogoRoute
|
|
||||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Login
|
object Login
|
||||||
|
|
||||||
@Serializable
|
|
||||||
object Logo
|
|
||||||
|
|
||||||
fun NavGraphBuilder.loginScreen(
|
fun NavGraphBuilder.loginScreen(
|
||||||
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
onNavigateToCaptcha: (CaptchaArguments) -> Unit,
|
||||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||||
onNavigateToMain: () -> Unit,
|
onNavigateToMain: () -> Unit,
|
||||||
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
onNavigateToUserBanned: (LoginUserBannedArguments) -> Unit,
|
||||||
onNavigateToCredentials: () -> Unit,
|
|
||||||
navController: NavController
|
navController: NavController
|
||||||
) {
|
) {
|
||||||
composable<Login> { backStackEntry ->
|
composable<Login> { backStackEntry ->
|
||||||
@@ -45,17 +40,6 @@ fun NavGraphBuilder.loginScreen(
|
|||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
composable<Logo> {
|
|
||||||
LogoRoute(
|
|
||||||
onNavigateToMain = onNavigateToMain,
|
|
||||||
onGoNextButtonClicked = onNavigateToCredentials
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun NavController.navigateToLogin() {
|
|
||||||
this.navigate(route = Login)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavBackStackEntry.getValidationResult(): String? {
|
fun NavBackStackEntry.getValidationResult(): String? {
|
||||||
|
|||||||
+115
-116
@@ -1,6 +1,10 @@
|
|||||||
package dev.meloda.fast.auth.login.presentation
|
package dev.meloda.fast.auth.login.presentation
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -28,13 +32,10 @@ import androidx.compose.material3.TextField
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.autofill.AutofillType
|
import androidx.compose.ui.autofill.ContentType
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
@@ -42,22 +43,23 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextRange
|
import androidx.compose.ui.semantics.contentType
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.os.bundleOf
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import dev.meloda.fast.auth.login.LoginViewModel
|
||||||
|
import dev.meloda.fast.auth.login.LoginViewModelImpl
|
||||||
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
import dev.meloda.fast.auth.login.model.CaptchaArguments
|
||||||
|
import dev.meloda.fast.auth.login.model.LoginDialog
|
||||||
import dev.meloda.fast.auth.login.model.LoginError
|
import dev.meloda.fast.auth.login.model.LoginError
|
||||||
import dev.meloda.fast.auth.login.model.LoginScreenState
|
import dev.meloda.fast.auth.login.model.LoginScreenState
|
||||||
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
import dev.meloda.fast.auth.login.model.LoginUserBannedArguments
|
||||||
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
import dev.meloda.fast.auth.login.model.LoginValidationArguments
|
||||||
import dev.meloda.fast.ui.basic.autoFillRequestHandler
|
|
||||||
import dev.meloda.fast.ui.basic.connectNode
|
|
||||||
import dev.meloda.fast.ui.basic.defaultFocusChangeAutoFill
|
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
import dev.meloda.fast.ui.components.TextFieldErrorText
|
import dev.meloda.fast.ui.components.TextFieldErrorText
|
||||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||||
@@ -74,50 +76,52 @@ fun LoginRoute(
|
|||||||
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
onNavigateToValidation: (LoginValidationArguments) -> Unit,
|
||||||
validationCode: String?,
|
validationCode: String?,
|
||||||
captchaCode: String?,
|
captchaCode: String?,
|
||||||
viewModel: dev.meloda.fast.auth.login.LoginViewModel = koinViewModel<dev.meloda.fast.auth.login.LoginViewModelImpl>()
|
viewModel: LoginViewModel = koinViewModel<LoginViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
||||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
||||||
val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
|
val userBannedArguments by viewModel.userBannedArguments.collectAsStateWithLifecycle()
|
||||||
val captchaArguments by viewModel.captchaArguments.collectAsStateWithLifecycle()
|
val captchaArguments by viewModel.captchaArguments.collectAsStateWithLifecycle()
|
||||||
val validationArguments by viewModel.validationArguments.collectAsStateWithLifecycle()
|
val validationArguments by viewModel.validationArguments.collectAsStateWithLifecycle()
|
||||||
val loginError by viewModel.loginError.collectAsStateWithLifecycle()
|
val loginDialog by viewModel.loginDialog.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
LaunchedEffect(isNeedToOpenMain) {
|
BackHandler(
|
||||||
|
enabled = !screenState.showLogo,
|
||||||
|
onBack = viewModel::onBackPressed
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(
|
||||||
|
isNeedToOpenMain,
|
||||||
|
userBannedArguments,
|
||||||
|
captchaArguments,
|
||||||
|
validationArguments,
|
||||||
|
validationCode,
|
||||||
|
captchaCode
|
||||||
|
) {
|
||||||
if (isNeedToOpenMain) {
|
if (isNeedToOpenMain) {
|
||||||
viewModel.onNavigatedToMain()
|
viewModel.onNavigatedToMain()
|
||||||
onNavigateToMain()
|
onNavigateToMain()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(userBannedArguments) {
|
|
||||||
userBannedArguments?.let { arguments ->
|
userBannedArguments?.let { arguments ->
|
||||||
viewModel.onNavigatedToUserBanned()
|
viewModel.onNavigatedToUserBanned()
|
||||||
onNavigateToUserBanned(arguments)
|
onNavigateToUserBanned(arguments)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(captchaArguments) {
|
|
||||||
captchaArguments?.let { arguments ->
|
captchaArguments?.let { arguments ->
|
||||||
viewModel.onNavigatedToCaptcha()
|
viewModel.onNavigatedToCaptcha()
|
||||||
onNavigateToCaptcha(arguments)
|
onNavigateToCaptcha(arguments)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(validationArguments) {
|
|
||||||
validationArguments?.let { arguments ->
|
validationArguments?.let { arguments ->
|
||||||
viewModel.onNavigatedToValidation()
|
viewModel.onNavigatedToValidation()
|
||||||
onNavigateToValidation(arguments)
|
onNavigateToValidation(arguments)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(validationCode) {
|
|
||||||
if (validationCode != null) {
|
if (validationCode != null) {
|
||||||
viewModel.onValidationCodeReceived(validationCode)
|
viewModel.onValidationCodeReceived(validationCode)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(captchaCode) {
|
|
||||||
if (captchaCode != null) {
|
if (captchaCode != null) {
|
||||||
viewModel.onCaptchaCodeReceived(captchaCode)
|
viewModel.onCaptchaCodeReceived(captchaCode)
|
||||||
}
|
}
|
||||||
@@ -125,89 +129,68 @@ fun LoginRoute(
|
|||||||
|
|
||||||
LoginScreen(
|
LoginScreen(
|
||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
onLoginAutoFilled = viewModel::onLoginInputChanged,
|
|
||||||
onPasswordAutoFilled = viewModel::onPasswordInputChanged,
|
|
||||||
onLoginInputChanged = viewModel::onLoginInputChanged,
|
onLoginInputChanged = viewModel::onLoginInputChanged,
|
||||||
onPasswordInputChanged = viewModel::onPasswordInputChanged,
|
onPasswordInputChanged = viewModel::onPasswordInputChanged,
|
||||||
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
|
onPasswordFieldEnterKeyClicked = viewModel::onSignInButtonClicked,
|
||||||
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
onPasswordVisibilityButtonClicked = viewModel::onPasswordVisibilityButtonClicked,
|
||||||
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
onPasswordFieldGoAction = viewModel::onSignInButtonClicked,
|
||||||
onSignInButtonClicked = viewModel::onSignInButtonClicked
|
onSignInButtonClicked = viewModel::onSignInButtonClicked,
|
||||||
|
onLogoLongClicked = viewModel::onLogoLongClicked
|
||||||
)
|
)
|
||||||
|
|
||||||
HandleError(
|
HandleDialogs(
|
||||||
onDismiss = viewModel::onErrorDialogDismissed,
|
loginDialog = loginDialog,
|
||||||
error = loginError
|
onConfirmed = viewModel::onDialogConfirmed,
|
||||||
|
onDismissed = viewModel::onDialogDismissed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
screenState: LoginScreenState = LoginScreenState.EMPTY,
|
screenState: LoginScreenState = LoginScreenState.EMPTY,
|
||||||
onLoginAutoFilled: (String) -> Unit = {},
|
|
||||||
onPasswordAutoFilled: (String) -> Unit = {},
|
|
||||||
onLoginInputChanged: (String) -> Unit = {},
|
onLoginInputChanged: (String) -> Unit = {},
|
||||||
onPasswordInputChanged: (String) -> Unit = {},
|
onPasswordInputChanged: (String) -> Unit = {},
|
||||||
onPasswordFieldEnterKeyClicked: () -> Unit = {},
|
onPasswordFieldEnterKeyClicked: () -> Unit = {},
|
||||||
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
onPasswordVisibilityButtonClicked: () -> Unit = {},
|
||||||
onPasswordFieldGoAction: () -> Unit = {},
|
onPasswordFieldGoAction: () -> Unit = {},
|
||||||
onSignInButtonClicked: () -> Unit = {}
|
onSignInButtonClicked: () -> Unit = {},
|
||||||
|
onLogoLongClicked: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val currentSize = LocalSizeConfig.current
|
val size = LocalSizeConfig.current
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val (loginFocusable, passwordFocusable) = FocusRequester.createRefs()
|
|
||||||
|
|
||||||
var loginText by remember { mutableStateOf(TextFieldValue(screenState.login)) }
|
val titleSpacerSize by animateDpAsState(if (size.isHeightSmall) 24.dp else 58.dp)
|
||||||
val showLoginError = screenState.loginError
|
val bottomPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
||||||
|
|
||||||
val autoFillEmailHandler = autoFillRequestHandler(
|
val (loginFocusable, passwordFocusable) =
|
||||||
autofillTypes = listOf(AutofillType.EmailAddress),
|
FocusRequester.createRefs()
|
||||||
onFill = { value ->
|
|
||||||
loginText = TextFieldValue(text = value, selection = TextRange(value.length))
|
|
||||||
onLoginAutoFilled(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var passwordText by remember { mutableStateOf(TextFieldValue(screenState.password)) }
|
|
||||||
val showPasswordError = screenState.passwordError
|
|
||||||
|
|
||||||
val autoFillPasswordHandler = autoFillRequestHandler(
|
|
||||||
autofillTypes = listOf(AutofillType.Password),
|
|
||||||
onFill = { value ->
|
|
||||||
passwordText = TextFieldValue(text = value, selection = TextRange(value.length))
|
|
||||||
onPasswordAutoFilled(value)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val titleStyle = if (currentSize.isWidthSmall) {
|
|
||||||
MaterialTheme.typography.displayMedium
|
|
||||||
} else {
|
|
||||||
MaterialTheme.typography.displayMedium
|
|
||||||
}
|
|
||||||
|
|
||||||
val titleSpacerSize = if (currentSize.isHeightSmall) {
|
|
||||||
24.dp
|
|
||||||
} else {
|
|
||||||
58.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
val bottomPadding = if (currentSize.isHeightSmall) {
|
|
||||||
10.dp
|
|
||||||
} else {
|
|
||||||
30.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
|
contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime)
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.padding(top = 30.dp)
|
.padding(top = 30.dp)
|
||||||
.padding(horizontal = 30.dp)
|
.padding(horizontal = 30.dp)
|
||||||
.padding(bottom = bottomPadding)
|
.padding(bottom = bottomPadding)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = screenState.showLogo,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Logo(onLogoLongClicked = onLogoLongClicked)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.Center),
|
||||||
|
visible = !screenState.showLogo,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -217,7 +200,7 @@ fun LoginScreen(
|
|||||||
Text(
|
Text(
|
||||||
text = stringResource(id = UiR.string.sign_in_to_vk),
|
text = stringResource(id = UiR.string.sign_in_to_vk),
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
style = titleStyle
|
style = MaterialTheme.typography.displayMedium
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(titleSpacerSize))
|
Spacer(modifier = Modifier.height(titleSpacerSize))
|
||||||
@@ -236,25 +219,18 @@ fun LoginScreen(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
.focusRequester(loginFocusable)
|
.focusRequester(loginFocusable)
|
||||||
.connectNode(handler = autoFillEmailHandler)
|
.semantics {
|
||||||
.defaultFocusChangeAutoFill(handler = autoFillEmailHandler),
|
contentType = ContentType.Username + ContentType.EmailAddress
|
||||||
value = loginText,
|
|
||||||
onValueChange = { newText ->
|
|
||||||
val text = newText.text
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
autoFillEmailHandler.requestVerifyManual()
|
|
||||||
}
|
|
||||||
|
|
||||||
loginText = newText
|
|
||||||
onLoginInputChanged(text)
|
|
||||||
},
|
},
|
||||||
|
value = screenState.login,
|
||||||
|
onValueChange = onLoginInputChanged,
|
||||||
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
label = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||||
placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = UiR.drawable.ic_round_person_24),
|
painter = painterResource(id = UiR.drawable.ic_round_person_24),
|
||||||
contentDescription = "Login icon",
|
contentDescription = "Login icon",
|
||||||
tint = if (showLoginError) {
|
tint = if (screenState.loginError) {
|
||||||
MaterialTheme.colorScheme.error
|
MaterialTheme.colorScheme.error
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
@@ -266,10 +242,10 @@ fun LoginScreen(
|
|||||||
keyboardType = KeyboardType.Email
|
keyboardType = KeyboardType.Email
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }),
|
keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }),
|
||||||
isError = showLoginError,
|
isError = screenState.loginError,
|
||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = showLoginError) {
|
AnimatedVisibility(visible = screenState.loginError) {
|
||||||
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
|
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,25 +262,16 @@ fun LoginScreen(
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
.focusRequester(passwordFocusable)
|
.focusRequester(passwordFocusable)
|
||||||
.connectNode(handler = autoFillPasswordHandler)
|
.semantics { contentType = ContentType.Password },
|
||||||
.defaultFocusChangeAutoFill(handler = autoFillPasswordHandler),
|
value = screenState.password,
|
||||||
value = passwordText,
|
onValueChange = onPasswordInputChanged,
|
||||||
onValueChange = { newText ->
|
|
||||||
val text = newText.text
|
|
||||||
if (text.isEmpty()) {
|
|
||||||
autoFillPasswordHandler.requestVerifyManual()
|
|
||||||
}
|
|
||||||
|
|
||||||
passwordText = newText
|
|
||||||
onPasswordInputChanged(text)
|
|
||||||
},
|
|
||||||
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
label = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||||
placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = UiR.drawable.round_vpn_key_24),
|
painter = painterResource(id = UiR.drawable.round_vpn_key_24),
|
||||||
contentDescription = "Password icon",
|
contentDescription = "Password icon",
|
||||||
tint = if (showPasswordError) {
|
tint = if (screenState.passwordError) {
|
||||||
MaterialTheme.colorScheme.error
|
MaterialTheme.colorScheme.error
|
||||||
} else {
|
} else {
|
||||||
MaterialTheme.colorScheme.primary
|
MaterialTheme.colorScheme.primary
|
||||||
@@ -335,7 +302,7 @@ fun LoginScreen(
|
|||||||
onPasswordFieldGoAction()
|
onPasswordFieldGoAction()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
isError = showPasswordError,
|
isError = screenState.passwordError,
|
||||||
visualTransformation = if (screenState.passwordVisible) {
|
visualTransformation = if (screenState.passwordVisible) {
|
||||||
VisualTransformation.None
|
VisualTransformation.None
|
||||||
} else {
|
} else {
|
||||||
@@ -343,19 +310,16 @@ fun LoginScreen(
|
|||||||
},
|
},
|
||||||
singleLine = true
|
singleLine = true
|
||||||
)
|
)
|
||||||
AnimatedVisibility(visible = showPasswordError) {
|
AnimatedVisibility(visible = screenState.passwordError) {
|
||||||
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
|
TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = !screenState.isLoading,
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut()
|
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -367,13 +331,6 @@ fun LoginScreen(
|
|||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
modifier = Modifier.testTag("sing_in_fab")
|
modifier = Modifier.testTag("sing_in_fab")
|
||||||
) {
|
) {
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
|
||||||
contentDescription = "Sign in icon",
|
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = screenState.isLoading,
|
visible = screenState.isLoading,
|
||||||
enter = fadeIn(),
|
enter = fadeIn(),
|
||||||
@@ -381,9 +338,51 @@ fun LoginScreen(
|
|||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !screenState.isLoading,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
||||||
|
contentDescription = "Sign in icon",
|
||||||
|
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HandleDialogs(
|
||||||
|
loginDialog: LoginDialog?,
|
||||||
|
onConfirmed: (LoginDialog, Bundle) -> Unit = { _, _ -> },
|
||||||
|
onDismissed: (LoginDialog) -> Unit = {},
|
||||||
|
) {
|
||||||
|
when (loginDialog) {
|
||||||
|
null -> Unit
|
||||||
|
|
||||||
|
is LoginDialog.Error -> {
|
||||||
|
MaterialDialog(
|
||||||
|
onDismissRequest = { onDismissed(loginDialog) },
|
||||||
|
title = stringResource(UiR.string.title_error),
|
||||||
|
text = loginDialog.errorTextResId?.let { stringResource(it) }
|
||||||
|
?: loginDialog.errorText.orEmpty(),
|
||||||
|
confirmText = stringResource(id = UiR.string.ok)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LoginDialog.FastAuth -> {
|
||||||
|
SignInAlert(
|
||||||
|
onDismissRequest = { onDismissed(loginDialog) },
|
||||||
|
onConfirmClick = { onConfirmed(loginDialog, bundleOf("token" to it)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package dev.meloda.fast.auth.login.presentation
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.width
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import dev.meloda.fast.ui.R
|
||||||
|
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Logo(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onLogoLongClicked: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
val size = LocalSizeConfig.current
|
||||||
|
|
||||||
|
val iconWidth by animateDpAsState(if (size.isWidthSmall) 110.dp else 134.dp)
|
||||||
|
val appNameFontSize by animateIntAsState(if (size.isWidthSmall) 40 else 40)
|
||||||
|
val bottomAdditionalPadding by animateDpAsState(if (size.isHeightSmall) 10.dp else 30.dp)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(top = 30.dp)
|
||||||
|
.padding(horizontal = 30.dp)
|
||||||
|
.padding(bottom = bottomAdditionalPadding)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.Center),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_logo_big),
|
||||||
|
contentDescription = "Application Logo",
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier
|
||||||
|
.width(iconWidth)
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = null,
|
||||||
|
indication = null,
|
||||||
|
onLongClick = onLogoLongClicked,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(46.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.fast_messenger),
|
||||||
|
style = MaterialTheme.typography.displayMedium.copy(fontSize = appNameFontSize.sp),
|
||||||
|
color = MaterialTheme.colorScheme.onBackground
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun LogoPreview() {
|
||||||
|
Logo()
|
||||||
|
}
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
package dev.meloda.fast.auth.login.presentation
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.combinedClickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
|
||||||
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.width
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.OutlinedTextField
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.testTag
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import dev.meloda.fast.auth.BuildConfig
|
|
||||||
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
|
||||||
import dev.meloda.fast.ui.components.MaterialDialog
|
|
||||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
import dev.meloda.fast.ui.R as UiR
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LogoRoute(
|
|
||||||
onNavigateToMain: () -> Unit,
|
|
||||||
onGoNextButtonClicked: () -> Unit,
|
|
||||||
viewModel: dev.meloda.fast.auth.login.LoginViewModel = koinViewModel<dev.meloda.fast.auth.login.LoginViewModelImpl>()
|
|
||||||
) {
|
|
||||||
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
|
|
||||||
val isNeedToOpenMain by viewModel.isNeedToOpenMain.collectAsStateWithLifecycle()
|
|
||||||
val isNeedToShowSignInAlert by viewModel.isNeedToShowFastSignInAlert.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
LaunchedEffect(isNeedToOpenMain) {
|
|
||||||
if (isNeedToOpenMain) {
|
|
||||||
viewModel.onNavigatedToMain()
|
|
||||||
onNavigateToMain()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LogoScreen(
|
|
||||||
isLoading = screenState.isLoading,
|
|
||||||
onLogoLongClicked = viewModel::onLogoLongClicked,
|
|
||||||
onGoNextButtonClicked = onGoNextButtonClicked
|
|
||||||
)
|
|
||||||
|
|
||||||
if (isNeedToShowSignInAlert) {
|
|
||||||
SignInAlert(
|
|
||||||
onDismissRequest = viewModel::onFastLogInAlertDismissed,
|
|
||||||
onConfirmClick = viewModel::onFastLogInAlertConfirmClicked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
|
||||||
@Composable
|
|
||||||
fun LogoScreen(
|
|
||||||
isLoading: Boolean = false,
|
|
||||||
onLogoLongClicked: () -> Unit = {},
|
|
||||||
onGoNextButtonClicked: () -> Unit = {}
|
|
||||||
) {
|
|
||||||
val currentSize = LocalSizeConfig.current
|
|
||||||
|
|
||||||
Scaffold { padding ->
|
|
||||||
val topPadding by animateDpAsState(
|
|
||||||
targetValue = padding.calculateTopPadding(),
|
|
||||||
label = "topPaddingAnimation"
|
|
||||||
)
|
|
||||||
val bottomPadding by animateDpAsState(
|
|
||||||
targetValue = padding.calculateBottomPadding(),
|
|
||||||
label = "bottomPaddingAnimation"
|
|
||||||
)
|
|
||||||
|
|
||||||
val endPadding by animateDpAsState(
|
|
||||||
targetValue = padding.calculateEndPadding(LayoutDirection.Ltr),
|
|
||||||
label = "endPaddingAnimation"
|
|
||||||
)
|
|
||||||
val startPadding by animateDpAsState(
|
|
||||||
targetValue = padding.calculateStartPadding(LayoutDirection.Ltr),
|
|
||||||
label = "startPaddingAnimation"
|
|
||||||
)
|
|
||||||
|
|
||||||
val iconWidth = if (currentSize.isWidthSmall) {
|
|
||||||
110.dp
|
|
||||||
} else {
|
|
||||||
134.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
val appNameTextStyle = if (currentSize.isWidthSmall) {
|
|
||||||
MaterialTheme.typography.displayMedium.copy(fontSize = 40.sp)
|
|
||||||
} else {
|
|
||||||
MaterialTheme.typography.displayMedium
|
|
||||||
}
|
|
||||||
|
|
||||||
val bottomAdditionalPadding = if (currentSize.isHeightSmall) {
|
|
||||||
10.dp
|
|
||||||
} else {
|
|
||||||
30.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(
|
|
||||||
start = startPadding,
|
|
||||||
top = topPadding,
|
|
||||||
end = endPadding,
|
|
||||||
bottom = bottomPadding
|
|
||||||
)
|
|
||||||
.padding(top = 30.dp)
|
|
||||||
.padding(horizontal = 30.dp)
|
|
||||||
.padding(bottom = bottomAdditionalPadding)
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.Center),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = UiR.drawable.ic_logo_big),
|
|
||||||
contentDescription = "Application Logo",
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier
|
|
||||||
.width(iconWidth)
|
|
||||||
.combinedClickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onLongClick = onLogoLongClicked,
|
|
||||||
onClick = {}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(46.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(id = UiR.string.fast_messenger),
|
|
||||||
style = appNameTextStyle,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = !isLoading,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter),
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut()
|
|
||||||
) {
|
|
||||||
FloatingActionButton(
|
|
||||||
onClick = {
|
|
||||||
if (!isLoading) {
|
|
||||||
onGoNextButtonClicked()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
|
||||||
modifier = Modifier.testTag("go_next_fab")
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(id = UiR.drawable.ic_arrow_end),
|
|
||||||
contentDescription = "Go button",
|
|
||||||
tint = MaterialTheme.colorScheme.onSecondaryContainer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isLoading,
|
|
||||||
modifier = Modifier.align(Alignment.BottomCenter)
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SignInAlert(
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
onConfirmClick: (token: String) -> Unit
|
|
||||||
) {
|
|
||||||
var tokenText by rememberSaveable {
|
|
||||||
mutableStateOf(BuildConfig.debugToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
val maxWidthModifier = Modifier.fillMaxWidth()
|
|
||||||
|
|
||||||
MaterialDialog(
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = "Fast authorization",
|
|
||||||
confirmText = stringResource(id = UiR.string.action_authorize),
|
|
||||||
confirmAction = { onConfirmClick(tokenText) },
|
|
||||||
cancelText = stringResource(id = UiR.string.cancel),
|
|
||||||
actionInvokeDismiss = ActionInvokeDismiss.Always
|
|
||||||
) {
|
|
||||||
Column(modifier = maxWidthModifier) {
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier = maxWidthModifier.padding(horizontal = 16.dp),
|
|
||||||
value = tokenText,
|
|
||||||
onValueChange = { tokenText = it },
|
|
||||||
placeholder = { Text(text = "Access token") },
|
|
||||||
label = { Text(text = "Access token") }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package dev.meloda.fast.auth.login.presentation
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.meloda.fast.auth.BuildConfig
|
||||||
|
import dev.meloda.fast.ui.components.ActionInvokeDismiss
|
||||||
|
import dev.meloda.fast.ui.components.MaterialDialog
|
||||||
|
|
||||||
|
import dev.meloda.fast.ui.R as UiR
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SignInAlert(
|
||||||
|
onDismissRequest: () -> Unit = {},
|
||||||
|
onConfirmClick: (token: String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
var tokenText by rememberSaveable {
|
||||||
|
mutableStateOf(BuildConfig.debugToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
val maxWidthModifier = Modifier.fillMaxWidth()
|
||||||
|
|
||||||
|
MaterialDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = "Fast authorization",
|
||||||
|
confirmText = stringResource(id = UiR.string.action_authorize),
|
||||||
|
confirmAction = { onConfirmClick(tokenText) },
|
||||||
|
cancelText = stringResource(id = UiR.string.cancel),
|
||||||
|
actionInvokeDismiss = ActionInvokeDismiss.Always
|
||||||
|
) {
|
||||||
|
Column(modifier = maxWidthModifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = maxWidthModifier.padding(horizontal = 16.dp),
|
||||||
|
value = tokenText,
|
||||||
|
onValueChange = { tokenText = it },
|
||||||
|
placeholder = { Text(text = "Access token") },
|
||||||
|
label = { Text(text = "Access token") }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-3
@@ -27,6 +27,7 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -52,6 +53,7 @@ import dev.meloda.fast.chatmaterials.presentation.materials.LinkMaterialsScreen
|
|||||||
import dev.meloda.fast.chatmaterials.presentation.materials.PhotoMaterialsScreen
|
import dev.meloda.fast.chatmaterials.presentation.materials.PhotoMaterialsScreen
|
||||||
import dev.meloda.fast.chatmaterials.presentation.materials.VideoMaterialsScreen
|
import dev.meloda.fast.chatmaterials.presentation.materials.VideoMaterialsScreen
|
||||||
import dev.meloda.fast.ui.model.TabItem
|
import dev.meloda.fast.ui.model.TabItem
|
||||||
|
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
@@ -118,9 +120,10 @@ fun ChatMaterialsScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val topBarContainerColor by animateColorAsState(
|
val topBarContainerColor by animateColorAsState(
|
||||||
targetValue =
|
targetValue = if (currentTheme.enableBlur || !canScrollBackward)
|
||||||
if (currentTheme.enableBlur || !canScrollBackward) MaterialTheme.colorScheme.surface
|
MaterialTheme.colorScheme.surface
|
||||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
else
|
||||||
|
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||||
label = "toolbarColorAlpha",
|
label = "toolbarColorAlpha",
|
||||||
animationSpec = tween(
|
animationSpec = tween(
|
||||||
durationMillis = 200,
|
durationMillis = 200,
|
||||||
@@ -204,6 +207,7 @@ fun ChatMaterialsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
|
CompositionLocalProvider(LocalHazeState provides hazeState) {
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -314,4 +318,5 @@ fun ChatMaterialsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -26,18 +26,19 @@ import dev.meloda.fast.conversations.model.ConversationsScreenState
|
|||||||
import dev.meloda.fast.data.UserConfig
|
import dev.meloda.fast.data.UserConfig
|
||||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||||
import dev.meloda.fast.ui.model.api.UiConversation
|
import dev.meloda.fast.ui.model.api.UiConversation
|
||||||
|
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ConversationsList(
|
fun ConversationsList(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onConversationsClick: (Int) -> Unit,
|
onConversationsClick: (Int) -> Unit,
|
||||||
onConversationsLongClick: (UiConversation) -> Unit,
|
onConversationsLongClick: (UiConversation) -> Unit,
|
||||||
screenState: ConversationsScreenState,
|
screenState: ConversationsScreenState,
|
||||||
state: LazyListState,
|
state: LazyListState,
|
||||||
maxLines: Int,
|
maxLines: Int,
|
||||||
modifier: Modifier,
|
|
||||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
|
onOptionClicked: (UiConversation, ConversationOption) -> Unit,
|
||||||
padding: PaddingValues
|
padding: PaddingValues
|
||||||
) {
|
) {
|
||||||
@@ -116,6 +117,7 @@ fun ConversationsList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.meloda.fast.friends.model.FriendsScreenState
|
import dev.meloda.fast.friends.model.FriendsScreenState
|
||||||
import dev.meloda.fast.ui.model.api.UiFriend
|
import dev.meloda.fast.ui.model.api.UiFriend
|
||||||
|
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||||
import dev.meloda.fast.ui.util.ImmutableList
|
import dev.meloda.fast.ui.util.ImmutableList
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -98,6 +99,7 @@ fun FriendsList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-6
@@ -71,7 +71,6 @@ interface MessagesHistoryViewModel {
|
|||||||
val canPaginate: StateFlow<Boolean>
|
val canPaginate: StateFlow<Boolean>
|
||||||
|
|
||||||
fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle)
|
fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle)
|
||||||
fun onDialogCancelled(dialog: MessageDialog)
|
|
||||||
fun onDialogDismissed(dialog: MessageDialog)
|
fun onDialogDismissed(dialog: MessageDialog)
|
||||||
fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle)
|
fun onDialogItemPicked(dialog: MessageDialog, bundle: Bundle)
|
||||||
|
|
||||||
@@ -151,7 +150,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) {
|
override fun onDialogConfirmed(dialog: MessageDialog, bundle: Bundle) {
|
||||||
messageDialog.setValue { null }
|
onDialogDismissed(dialog)
|
||||||
|
|
||||||
when (dialog) {
|
when (dialog) {
|
||||||
is MessageDialog.MessageOptions -> Unit
|
is MessageDialog.MessageOptions -> Unit
|
||||||
@@ -223,10 +222,6 @@ class MessagesHistoryViewModelImpl(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDialogCancelled(dialog: MessageDialog) {
|
|
||||||
messageDialog.setValue { null }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDialogDismissed(dialog: MessageDialog) {
|
override fun onDialogDismissed(dialog: MessageDialog) {
|
||||||
messageDialog.setValue { null }
|
messageDialog.setValue { null }
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -1,7 +1,9 @@
|
|||||||
package dev.meloda.fast.messageshistory.model
|
package dev.meloda.fast.messageshistory.model
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
import dev.meloda.fast.model.api.domain.VkMessage
|
import dev.meloda.fast.model.api.domain.VkMessage
|
||||||
|
|
||||||
|
@Immutable
|
||||||
sealed class MessageDialog {
|
sealed class MessageDialog {
|
||||||
data class MessageOptions(val message: VkMessage) : MessageDialog()
|
data class MessageOptions(val message: VkMessage) : MessageDialog()
|
||||||
data class MessagePin(val messageId: Int) : MessageDialog()
|
data class MessagePin(val messageId: Int) : MessageDialog()
|
||||||
|
|||||||
-2
@@ -170,7 +170,6 @@ fun MessagesHistoryRoute(
|
|||||||
screenState = screenState,
|
screenState = screenState,
|
||||||
messageDialog = messageDialog,
|
messageDialog = messageDialog,
|
||||||
onConfirmed = viewModel::onDialogConfirmed,
|
onConfirmed = viewModel::onDialogConfirmed,
|
||||||
onCancelled = viewModel::onDialogCancelled,
|
|
||||||
onDismissed = viewModel::onDialogDismissed,
|
onDismissed = viewModel::onDialogDismissed,
|
||||||
onItemPicked = viewModel::onDialogItemPicked
|
onItemPicked = viewModel::onDialogItemPicked
|
||||||
)
|
)
|
||||||
@@ -181,7 +180,6 @@ fun HandleDialogs(
|
|||||||
screenState: MessagesHistoryScreenState,
|
screenState: MessagesHistoryScreenState,
|
||||||
messageDialog: MessageDialog?,
|
messageDialog: MessageDialog?,
|
||||||
onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> },
|
onConfirmed: (MessageDialog, Bundle) -> Unit = { _, _ -> },
|
||||||
onCancelled: (MessageDialog) -> Unit = {},
|
|
||||||
onDismissed: (MessageDialog) -> Unit = {},
|
onDismissed: (MessageDialog) -> Unit = {},
|
||||||
onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> }
|
onItemPicked: (MessageDialog, Bundle) -> Unit = { _, _ -> }
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -30,12 +30,8 @@ retrofit = "2.11.0"
|
|||||||
room = "2.6.1"
|
room = "2.6.1"
|
||||||
preference-ktx = "1.2.1"
|
preference-ktx = "1.2.1"
|
||||||
nanokt = "1.2.0"
|
nanokt = "1.2.0"
|
||||||
junitVersion = "1.2.1"
|
|
||||||
espressoCore = "3.6.1"
|
|
||||||
appcompat = "1.7.0"
|
|
||||||
androidx-navigation = "2.8.9"
|
androidx-navigation = "2.8.9"
|
||||||
serialization = "1.8.0"
|
serialization = "1.8.0"
|
||||||
rebugger = "1.0.0-rc03"
|
|
||||||
moduleGraph = "2.8.0"
|
moduleGraph = "2.8.0"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
@@ -69,18 +65,13 @@ preference-ktx = { module = "androidx.preference:preference-ktx", version.ref =
|
|||||||
nanokt = { module = "com.conena.nanokt:nanokt", version.ref = "nanokt" }
|
nanokt = { module = "com.conena.nanokt:nanokt", version.ref = "nanokt" }
|
||||||
nanokt-android = { module = "com.conena.nanokt:nanokt-android", version.ref = "nanokt" }
|
nanokt-android = { module = "com.conena.nanokt:nanokt-android", version.ref = "nanokt" }
|
||||||
nanokt-jvm = { module = "com.conena.nanokt:nanokt-jvm", version.ref = "nanokt" }
|
nanokt-jvm = { module = "com.conena.nanokt:nanokt-jvm", version.ref = "nanokt" }
|
||||||
ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
|
|
||||||
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
|
|
||||||
appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
|
|
||||||
|
|
||||||
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
|
||||||
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
|
|
||||||
rebugger = { module = "io.github.theapache64:rebugger-android", version.ref = "rebugger" }
|
|
||||||
|
|
||||||
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
|
compose-bom = { module = "androidx.compose:compose-bom", version.ref = "compose-bom" }
|
||||||
compose-material3 = { module = "androidx.compose.material3:material3" }
|
compose-material3 = { module = "androidx.compose.material3:material3" }
|
||||||
compose-ui = { module = "androidx.compose.ui:ui" }
|
compose-ui = { module = "androidx.compose.ui:ui", version = "1.8.0-rc02" }
|
||||||
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
|
||||||
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
|
||||||
compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" }
|
compose-material3-windowsize = { module = "androidx.compose.material3:material3-window-size-class" }
|
||||||
@@ -92,10 +83,7 @@ compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
|||||||
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||||
|
|
||||||
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
|
||||||
koin-core-coroutines = { module = "io.insert-koin:koin-core-coroutines", version.ref = "koin" }
|
|
||||||
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
|
|
||||||
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
|
||||||
koin-android-test = { module = "io.insert-koin:koin-android-test", version.ref = "koin" }
|
|
||||||
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
|
||||||
koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koin" }
|
koin-androidx-compose-navigation = { module = "io.insert-koin:koin-androidx-compose-navigation", version.ref = "koin" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user