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:
nk
2026-06-02 21:31:16 +03:00
parent d652dc399a
commit f604ad42e8
16 changed files with 1195 additions and 499 deletions

View File

@@ -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")
}
}