diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0e132007..95861bfd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -181,4 +181,6 @@ dependencies { implementation(libs.androidx.navigation.compose) implementation(libs.kotlin.serialization) + + implementation("androidx.compose.material3.adaptive:adaptive:1.0.0-beta04") } diff --git a/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt index 0e04ba77..c4158474 100644 --- a/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt +++ b/app/src/main/kotlin/com/meloda/app/fast/presentation/MainActivity.kt @@ -15,6 +15,7 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -28,6 +29,7 @@ import androidx.core.content.ContextCompat import androidx.core.os.LocaleListCompat import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.window.core.layout.WindowWidthSizeClass import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.meloda.app.fast.MainViewModel @@ -57,7 +59,11 @@ class MainActivity : AppCompatActivity() { val currentNightMode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK val systemBarStyle = when (currentNightMode) { - Configuration.UI_MODE_NIGHT_NO -> SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb()) + Configuration.UI_MODE_NIGHT_NO -> SystemBarStyle.light( + Color.Transparent.toArgb(), + Color.Transparent.toArgb() + ) + Configuration.UI_MODE_NIGHT_YES -> SystemBarStyle.dark(Color.Transparent.toArgb()) else -> error("Illegal State, current mode is $currentNightMode") } @@ -104,6 +110,14 @@ class MainActivity : AppCompatActivity() { } } + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + + val isDeviceCompact by remember(windowAdaptiveInfo) { + derivedStateOf { + windowAdaptiveInfo.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT + } + } + val theme by userSettings.theme.collectAsStateWithLifecycle() CompositionLocalProvider( LocalTheme provides ThemeConfig( @@ -112,7 +126,8 @@ class MainActivity : AppCompatActivity() { selectedColorScheme = theme.selectedColorScheme, usingAmoledBackground = theme.usingAmoledBackground, usingBlur = theme.usingBlur, - multiline = theme.multiline + multiline = theme.multiline, + isDeviceCompact = isDeviceCompact ) ) { val currentTheme = LocalTheme.current diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt index 70a4718c..ef6e341a 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/UserSettings.kt @@ -40,7 +40,8 @@ class UserSettingsImpl( selectedColorScheme = selectedColorScheme(), usingAmoledBackground = isUsingAmoledBackground(), usingBlur = isUsingBlur(), - multiline = isMultiline() + multiline = isMultiline(), + isDeviceCompact = false ) ) diff --git a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt index cc94c419..a63bbb5c 100644 --- a/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt +++ b/core/datastore/src/main/kotlin/com/meloda/app/fast/datastore/model/ThemeConfig.kt @@ -7,5 +7,5 @@ data class ThemeConfig( val usingAmoledBackground: Boolean, val usingBlur: Boolean, val multiline: Boolean, - val bubblesWithPinch: Boolean = true + val isDeviceCompact: Boolean ) diff --git a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt index 9e730f51..30f9f8d9 100644 --- a/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt +++ b/core/designsystem/src/main/kotlin/com/meloda/app/fast/designsystem/AppTheme.kt @@ -110,7 +110,8 @@ val LocalTheme = compositionLocalOf { selectedColorScheme = 0, usingAmoledBackground = false, usingBlur = false, - multiline = false + multiline = false, + isDeviceCompact = false ) } diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt index 49556741..3d8311cd 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LoginScreen.kt @@ -6,11 +6,13 @@ import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.union import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -20,6 +22,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -48,6 +51,7 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.meloda.app.fast.common.UiText +import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.app.fast.designsystem.MaterialDialog import com.meloda.app.fast.designsystem.TextFieldErrorText import com.meloda.app.fast.designsystem.autoFillRequestHandler @@ -60,8 +64,8 @@ import com.meloda.fast.auth.login.LoginViewModelImpl import com.meloda.fast.auth.login.model.CaptchaArguments import com.meloda.fast.auth.login.model.LoginError import com.meloda.fast.auth.login.model.LoginScreenState -import com.meloda.fast.auth.login.model.LoginValidationArguments import com.meloda.fast.auth.login.model.LoginUserBannedArguments +import com.meloda.fast.auth.login.model.LoginValidationArguments import org.koin.androidx.compose.koinViewModel import com.meloda.app.fast.designsystem.R as UiR @@ -153,6 +157,7 @@ fun LoginScreen( onPasswordFieldGoAction: () -> Unit = {}, onSignInButtonClicked: () -> Unit = {} ) { + val currentTheme = LocalTheme.current val focusManager = LocalFocusManager.current val (loginFocusable, passwordFocusable) = FocusRequester.createRefs() @@ -181,183 +186,198 @@ fun LoginScreen( } ) - Scaffold { padding -> + val titleStyle = if (currentTheme.isDeviceCompact) { + MaterialTheme.typography.displaySmall + } else { + MaterialTheme.typography.displayMedium + } + + val titleSpacerSize = if (currentTheme.isDeviceCompact) { + 24.dp + } else { + 58.dp + } + + val bottomPadding = if (currentTheme.isDeviceCompact) { + 10.dp + } else { + 30.dp + } + + Scaffold( + contentWindowInsets = ScaffoldDefaults.contentWindowInsets.union(WindowInsets.ime) + ) { padding -> Box( modifier = Modifier .fillMaxSize() .padding(padding) + .padding(top = 30.dp) + .padding(horizontal = 30.dp) + .padding(bottom = bottomPadding) ) { - Box( + Column( modifier = Modifier - .fillMaxSize() - .padding(30.dp) - .imePadding() + .fillMaxWidth() + .align(Alignment.Center) ) { - Column( + Text( + text = stringResource(id = UiR.string.sign_in_to_vk), + color = MaterialTheme.colorScheme.onBackground, + style = titleStyle + ) + + Spacer(modifier = Modifier.height(titleSpacerSize)) + + TextField( modifier = Modifier + .height(58.dp) .fillMaxWidth() - .align(Alignment.Center) - ) { - Text( - text = stringResource(id = UiR.string.sign_in_to_vk), - color = MaterialTheme.colorScheme.onBackground, - style = MaterialTheme.typography.displayMedium - ) + .clip(RoundedCornerShape(10.dp)) + .handleEnterKey { + passwordFocusable.requestFocus() + true + } + .handleTabKey { + passwordFocusable.requestFocus() + true + } + .focusRequester(loginFocusable) + .connectNode(handler = autoFillEmailHandler) + .defaultFocusChangeAutoFill(handler = autoFillEmailHandler), + value = loginText, + onValueChange = { newText -> + val text = newText.text + if (text.isEmpty()) { + autoFillEmailHandler.requestVerifyManual() + } - Spacer(modifier = Modifier.height(58.dp)) - - TextField( - modifier = Modifier - .height(58.dp) - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey { - passwordFocusable.requestFocus() - true + loginText = newText + onLoginInputChanged(text) + }, + label = { Text(text = stringResource(id = UiR.string.login_hint)) }, + placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) }, + leadingIcon = { + Icon( + painter = painterResource(id = UiR.drawable.ic_round_person_24), + contentDescription = "Login icon", + tint = if (showLoginError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary } - .handleTabKey { - passwordFocusable.requestFocus() - true - } - .focusRequester(loginFocusable) - .connectNode(handler = autoFillEmailHandler) - .defaultFocusChangeAutoFill(handler = autoFillEmailHandler), - value = loginText, - onValueChange = { newText -> - val text = newText.text - if (text.isEmpty()) { - autoFillEmailHandler.requestVerifyManual() - } - - loginText = newText - onLoginInputChanged(text) - }, - label = { Text(text = stringResource(id = UiR.string.login_hint)) }, - placeholder = { Text(text = stringResource(id = UiR.string.login_hint)) }, - leadingIcon = { - Icon( - painter = painterResource(id = UiR.drawable.ic_round_person_24), - contentDescription = "Login icon", - tint = if (showLoginError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Next, - keyboardType = KeyboardType.Email - ), - keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }), - isError = showLoginError, - singleLine = true - ) - AnimatedVisibility(visible = showLoginError) { - TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - TextField( - modifier = Modifier - .height(58.dp) - .fillMaxWidth() - .clip(RoundedCornerShape(10.dp)) - .handleEnterKey { - focusManager.clearFocus() - onPasswordFieldEnterKeyClicked() - true - } - .focusRequester(passwordFocusable) - .connectNode(handler = autoFillPasswordHandler) - .defaultFocusChangeAutoFill(handler = autoFillPasswordHandler), - value = passwordText, - 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)) }, - placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, - leadingIcon = { - Icon( - painter = painterResource(id = UiR.drawable.round_vpn_key_24), - contentDescription = "Password icon", - tint = if (showPasswordError) { - MaterialTheme.colorScheme.error - } else { - MaterialTheme.colorScheme.primary - } - ) - }, - trailingIcon = { - val imagePainter = painterResource( - id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24 - else UiR.drawable.round_visibility_24 - ) - - IconButton(onClick = onPasswordVisibilityButtonClicked) { - Icon( - painter = imagePainter, - contentDescription = if (screenState.passwordVisible) "Password visible icon" - else "Password invisible icon" - ) - } - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = ImeAction.Go, - keyboardType = KeyboardType.Password - ), - keyboardActions = KeyboardActions( - onGo = { - focusManager.clearFocus() - onPasswordFieldGoAction() - } - ), - isError = showPasswordError, - visualTransformation = if (screenState.passwordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - singleLine = true - ) - AnimatedVisibility(visible = showPasswordError) { - TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) - } + ) + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Next, + keyboardType = KeyboardType.Email + ), + keyboardActions = KeyboardActions(onNext = { passwordFocusable.requestFocus() }), + isError = showLoginError, + singleLine = true + ) + AnimatedVisibility(visible = showLoginError) { + TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) } - Box( - modifier = Modifier.align(Alignment.BottomCenter), - contentAlignment = Alignment.Center - ) { + Spacer(modifier = Modifier.height(16.dp)) - FloatingActionButton( - onClick = { + TextField( + modifier = Modifier + .height(58.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .handleEnterKey { focusManager.clearFocus() - onSignInButtonClicked() - }, - containerColor = MaterialTheme.colorScheme.secondaryContainer, - modifier = Modifier.testTag("sing_in_fab") - ) { + onPasswordFieldEnterKeyClicked() + true + } + .focusRequester(passwordFocusable) + .connectNode(handler = autoFillPasswordHandler) + .defaultFocusChangeAutoFill(handler = autoFillPasswordHandler), + value = passwordText, + 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)) }, + placeholder = { Text(text = stringResource(id = UiR.string.password_login_hint)) }, + leadingIcon = { Icon( - painter = painterResource(id = UiR.drawable.ic_arrow_end), - contentDescription = "Sign in icon", - tint = MaterialTheme.colorScheme.onSecondaryContainer + painter = painterResource(id = UiR.drawable.round_vpn_key_24), + contentDescription = "Password icon", + tint = if (showPasswordError) { + MaterialTheme.colorScheme.error + } else { + MaterialTheme.colorScheme.primary + } ) - } - AnimatedVisibility( - visible = screenState.isLoading, - enter = fadeIn(), - exit = fadeOut() - ) { - CircularProgressIndicator() - } + }, + trailingIcon = { + val imagePainter = painterResource( + id = if (screenState.passwordVisible) UiR.drawable.round_visibility_off_24 + else UiR.drawable.round_visibility_24 + ) + + IconButton(onClick = onPasswordVisibilityButtonClicked) { + Icon( + painter = imagePainter, + contentDescription = if (screenState.passwordVisible) "Password visible icon" + else "Password invisible icon" + ) + } + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Go, + keyboardType = KeyboardType.Password + ), + keyboardActions = KeyboardActions( + onGo = { + focusManager.clearFocus() + onPasswordFieldGoAction() + } + ), + isError = showPasswordError, + visualTransformation = if (screenState.passwordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + singleLine = true + ) + AnimatedVisibility(visible = showPasswordError) { + TextFieldErrorText(text = stringResource(id = UiR.string.error_empty_field)) + } + } + + Box( + modifier = Modifier.align(Alignment.BottomCenter), + contentAlignment = Alignment.Center + ) { + FloatingActionButton( + onClick = { + focusManager.clearFocus() + onSignInButtonClicked() + }, + containerColor = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.testTag("sing_in_fab") + ) { + Icon( + painter = painterResource(id = UiR.drawable.ic_arrow_end), + contentDescription = "Sign in icon", + tint = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + AnimatedVisibility( + visible = screenState.isLoading, + enter = fadeIn(), + exit = fadeOut() + ) { + CircularProgressIndicator() } } } diff --git a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt index 24e4473f..eb2297f2 100644 --- a/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt +++ b/feature/auth/login/src/main/kotlin/com/meloda/fast/auth/login/presentation/LogoScreen.kt @@ -13,6 +13,7 @@ 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.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -30,6 +31,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.meloda.app.fast.designsystem.LocalTheme import com.meloda.fast.auth.login.LoginViewModel import com.meloda.fast.auth.login.LoginViewModelImpl import org.koin.androidx.compose.koinViewModel @@ -64,6 +66,8 @@ fun LogoScreen( onLogoLongClicked: () -> Unit = {}, onGoNextButtonClicked: () -> Unit = {} ) { + val currentTheme = LocalTheme.current + Scaffold { padding -> val topPadding by animateDpAsState( targetValue = padding.calculateTopPadding(), @@ -83,6 +87,24 @@ fun LogoScreen( label = "startPaddingAnimation" ) + val iconWidth = if (currentTheme.isDeviceCompact) { + 100.dp + } else { + 134.dp + } + + val appNameTextStyle = if (currentTheme.isDeviceCompact) { + MaterialTheme.typography.displaySmall + } else { + MaterialTheme.typography.displayMedium + } + + val bottomAdditionalPadding = if (currentTheme.isDeviceCompact) { + 10.dp + } else { + 30.dp + } + Box( modifier = Modifier .fillMaxSize() @@ -92,7 +114,9 @@ fun LogoScreen( end = endPadding, bottom = bottomPadding ) - .padding(30.dp) + .padding(top = 30.dp) + .padding(horizontal = 30.dp) + .padding(bottom = bottomAdditionalPadding) ) { Column( modifier = Modifier @@ -104,17 +128,19 @@ fun LogoScreen( painter = painterResource(id = UiR.drawable.ic_logo_big), contentDescription = "Application Logo", tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.combinedClickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onLongClick = onLogoLongClicked, - onClick = {} - ) + 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 = MaterialTheme.typography.displayMedium, + style = appNameTextStyle, color = MaterialTheme.colorScheme.onBackground ) } diff --git a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 6e28a7fe..b2a6153f 100644 --- a/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/com/meloda/app/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -163,16 +164,16 @@ fun MessagesHistoryScreen( ) } + val toolbarColorAlpha by animateFloatAsState( + targetValue = if (!listState.canScrollForward) 1f else 0f, + label = "toolbarColorAlpha", + animationSpec = tween(durationMillis = 50) + ) + Scaffold( modifier = Modifier.fillMaxSize(), contentWindowInsets = WindowInsets.statusBars, topBar = { - val toolbarColorAlpha by animateFloatAsState( - targetValue = if (!listState.canScrollForward) 1f else 0f, - label = "toolbarColorAlpha", - animationSpec = tween(durationMillis = 50) - ) - Column(modifier = Modifier.fillMaxWidth()) { TopAppBar( modifier = Modifier @@ -370,7 +371,13 @@ fun MessagesHistoryScreen( unfocusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent, ), - placeholder = { Text(text = stringResource(id = UiR.string.message_input_hint)) } + placeholder = { + Text( + text = stringResource(id = UiR.string.message_input_hint), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } ) IconButton(onClick = onAttachmentButtonClicked) {