feat(ui): рестайл всех экранов + плеер + официальные mono-логотипы сервисов
- экраны (Станции/Избранное/История/Записи/Настройки/Вход): двухцветные заголовки, токены темы, EmptyState, анимации появления и перестановки - AuthScreen: брендовый локап (AppMark + RadiolaWordmark) - PlayerBottomSheet: живой эфир — LiveEqualizer вместо перемотки, Crossfade трека и play/pause, pressScale, анимация избранного/записи - кнопки музыкальных сервисов: монохромные официальные логотипы (vector drawable из Simple Icons CC0 + Yandex), маппинг serviceLogoRes - DeeplinkBottomSheet: сетка сервисов с логотипами
This commit is contained in:
@@ -1,16 +1,25 @@
|
||||
package com.radiola.ui.auth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.radiola.ui.theme.AppMark
|
||||
import com.radiola.ui.theme.RadiolaTheme
|
||||
import com.radiola.ui.theme.RadiolaWordmark
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -23,6 +32,10 @@ fun AuthScreen(
|
||||
val email by viewModel.email.collectAsState()
|
||||
var code by remember { mutableStateOf("") }
|
||||
var showCodeInput by remember { mutableStateOf(false) }
|
||||
// Отслеживаем фокус на полях ввода для акцентной рамки
|
||||
var emailFocused by remember { mutableStateOf(false) }
|
||||
var codeFocused by remember { mutableStateOf(false) }
|
||||
val colors = RadiolaTheme.colors
|
||||
|
||||
LaunchedEffect(state) {
|
||||
when (state) {
|
||||
@@ -32,36 +45,48 @@ fun AuthScreen(
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Вход в radiOLA") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(colors.bgBase),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// Логотип
|
||||
AppMark(size = 84.dp)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
RadiolaWordmark(fontSize = 26)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
|
||||
// Заголовок
|
||||
Text(
|
||||
text = if (showCodeInput) "Введите код из письма" else "Добро пожаловать",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = colors.textPrimary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Подзаголовок
|
||||
Text(
|
||||
text = if (showCodeInput) "Мы отправили 6-значный код на ваш email" else "Войдите, чтобы синхронизировать избранное и историю между устройствами",
|
||||
text = if (showCodeInput)
|
||||
"Мы отправили 6-значный код на $email"
|
||||
else
|
||||
"Войдите, чтобы синхронизировать избранное и историю",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = colors.textSecondary,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
if (!showCodeInput) {
|
||||
// Поле ввода email с акцентной рамкой при фокусе
|
||||
OutlinedTextField(
|
||||
value = email,
|
||||
onValueChange = viewModel::onEmailChange,
|
||||
@@ -72,18 +97,43 @@ fun AuthScreen(
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = { viewModel.requestCode() }),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { emailFocused = it.isFocused },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = colors.accent,
|
||||
unfocusedBorderColor = colors.border,
|
||||
focusedLabelColor = colors.accent,
|
||||
unfocusedLabelColor = colors.textSecondary,
|
||||
focusedTextColor = colors.textPrimary,
|
||||
unfocusedTextColor = colors.textPrimary,
|
||||
cursorColor = colors.accent,
|
||||
focusedContainerColor = colors.surface,
|
||||
unfocusedContainerColor = colors.surface
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.requestCode() },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = state !is AuthViewModel.AuthState.Loading
|
||||
enabled = state !is AuthViewModel.AuthState.Loading,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = colors.accent,
|
||||
contentColor = colors.bgBase,
|
||||
disabledContainerColor = colors.accentDim.copy(alpha = 0.5f),
|
||||
disabledContentColor = colors.bgBase.copy(alpha = 0.5f)
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
if (state is AuthViewModel.AuthState.Loading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = colors.bgBase,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("Получить код")
|
||||
Text("Получить код", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,16 +142,15 @@ fun AuthScreen(
|
||||
onClick = onSkip,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Продолжить без входа")
|
||||
Text(
|
||||
"Продолжить без входа",
|
||||
color = colors.textSecondary,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "Код отправлен на $email",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
// Поле ввода кода с акцентной рамкой при фокусе
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = { if (it.length <= 6) code = it.uppercase() },
|
||||
@@ -111,21 +160,44 @@ fun AuthScreen(
|
||||
keyboardType = KeyboardType.NumberPassword,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
keyboardActions = KeyboardActions(onDone = {
|
||||
viewModel.verifyCode(code)
|
||||
}),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
keyboardActions = KeyboardActions(onDone = { viewModel.verifyCode(code) }),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.onFocusChanged { codeFocused = it.isFocused },
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = colors.accent,
|
||||
unfocusedBorderColor = colors.border,
|
||||
focusedLabelColor = colors.accent,
|
||||
unfocusedLabelColor = colors.textSecondary,
|
||||
focusedTextColor = colors.textPrimary,
|
||||
unfocusedTextColor = colors.textPrimary,
|
||||
cursorColor = colors.accent,
|
||||
focusedContainerColor = colors.surface,
|
||||
unfocusedContainerColor = colors.surface
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.verifyCode(code) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = state !is AuthViewModel.AuthState.Loading && code.length == 6
|
||||
enabled = state !is AuthViewModel.AuthState.Loading && code.length == 6,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = colors.accent,
|
||||
contentColor = colors.bgBase,
|
||||
disabledContainerColor = colors.accentDim.copy(alpha = 0.5f),
|
||||
disabledContentColor = colors.bgBase.copy(alpha = 0.5f)
|
||||
),
|
||||
shape = RoundedCornerShape(14.dp)
|
||||
) {
|
||||
if (state is AuthViewModel.AuthState.Loading) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(20.dp))
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = colors.bgBase,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("Войти")
|
||||
Text("Войти", style = MaterialTheme.typography.labelLarge)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,20 +208,29 @@ fun AuthScreen(
|
||||
viewModel.dismissError()
|
||||
}
|
||||
) {
|
||||
Text("Отправить код повторно")
|
||||
Text(
|
||||
"Отправить код повторно",
|
||||
color = colors.textSecondary,
|
||||
style = MaterialTheme.typography.labelLarge
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Диалог ошибки
|
||||
if (state is AuthViewModel.AuthState.Error) {
|
||||
val errorMessage = (state as AuthViewModel.AuthState.Error).message
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::dismissError,
|
||||
title = { Text("Ошибка") },
|
||||
text = { Text(errorMessage) },
|
||||
containerColor = colors.elevated,
|
||||
title = { Text("Ошибка", color = colors.textPrimary) },
|
||||
text = { Text(errorMessage, color = colors.textSecondary) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = viewModel::dismissError) {
|
||||
TextButton(
|
||||
onClick = viewModel::dismissError,
|
||||
colors = ButtonDefaults.textButtonColors(contentColor = colors.accent)
|
||||
) {
|
||||
Text("OK")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user