package com.radiola.ui.settings import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape 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.graphics.Color import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import com.composables.icons.lucide.AlarmClock import com.composables.icons.lucide.ChevronRight import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.SlidersHorizontal import com.composables.icons.lucide.User import com.radiola.domain.model.DeeplinkService import com.radiola.domain.model.StationTestStatus import com.radiola.ui.theme.Motion import com.radiola.ui.theme.RadiolaTheme import com.radiola.ui.theme.ThemePalette @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( onNavigateToAuth: () -> Unit, onNavigateToAlarms: () -> Unit = {}, onNavigateToEqualizer: () -> Unit = {}, modifier: Modifier = Modifier, viewModel: SettingsViewModel = hiltViewModel() ) { val sleepTimer by viewModel.sleepTimerMinutes.collectAsState() val enabledServices by viewModel.enabledServices.collectAsState() val visualizerStyle by viewModel.visualizerStyle.collectAsState() val themePalette by viewModel.themePalette.collectAsState() val preferredBitrate by viewModel.preferredBitrate.collectAsState() val isTesting by viewModel.isTesting.collectAsState() val testProgress by viewModel.testProgress.collectAsState() val testTotal by viewModel.testTotal.collectAsState() val testResults by viewModel.testResults.collectAsState() val isLoggedIn by viewModel.isLoggedIn.collectAsState() val currentUser by viewModel.currentUser.collectAsState() var showReport by remember { mutableStateOf(false) } val colors = RadiolaTheme.colors LazyColumn( modifier = modifier .fillMaxSize() .padding(horizontal = 20.dp), contentPadding = PaddingValues(bottom = 32.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { item { // Двухцветный заголовок Text( text = buildAnnotatedString { withStyle(SpanStyle(color = colors.textPrimary)) { append("Нас") } withStyle(SpanStyle(color = colors.accent)) { append("тройки") } }, style = MaterialTheme.typography.headlineLarge, modifier = Modifier.padding(top = 20.dp) ) } // --- Тема оформления --- item { SectionLabel("ТЕМА ОФОРМЛЕНИЯ") Spacer(Modifier.height(8.dp)) Row( modifier = Modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { ThemePalette.entries.forEach { palette -> ThemeSwatch( palette = palette, selected = themePalette == palette.id, onClick = { viewModel.setThemePalette(palette.id) } ) } } } // --- Профиль --- item { SectionLabel("ПРОФИЛЬ") Spacer(Modifier.height(8.dp)) Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) .padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { // Аватар-заглушка Box( modifier = Modifier .size(48.dp) .clip(CircleShape) .background(colors.surface2), contentAlignment = Alignment.Center ) { Icon(Lucide.User, null, tint = colors.textMuted, modifier = Modifier.size(22.dp)) } Column(modifier = Modifier.weight(1f)) { if (isLoggedIn && currentUser != null) { Text( text = currentUser?.email ?: "", style = MaterialTheme.typography.titleMedium, color = colors.textPrimary ) Text( text = "Аккаунт активен", style = MaterialTheme.typography.labelMedium, color = colors.textSecondary ) } else { Text( text = "Вы не вошли", style = MaterialTheme.typography.titleMedium, color = colors.textPrimary ) Text( text = "Вход не обязателен — для синхронизации", style = MaterialTheme.typography.labelMedium, color = colors.textSecondary ) } } if (isLoggedIn && currentUser != null) { OutlinedButton( onClick = { viewModel.logout() }, colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.textSecondary), border = androidx.compose.foundation.BorderStroke(1.dp, colors.border) ) { Text("Выйти") } } else { Button( onClick = onNavigateToAuth, colors = ButtonDefaults.buttonColors( containerColor = colors.accent, contentColor = colors.bgBase ), shape = RoundedCornerShape(10.dp) ) { Text("Войти", fontWeight = FontWeight.SemiBold) } } } } // --- Будильник --- item { SectionLabel("БУДИЛЬНИК") Spacer(Modifier.height(8.dp)) Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) .clickable { onNavigateToAlarms() } .padding(horizontal = 16.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { Icon( Lucide.AlarmClock, contentDescription = null, tint = colors.accent, modifier = Modifier.size(22.dp) ) Column(modifier = Modifier.weight(1f)) { Text( text = "Будильники", style = MaterialTheme.typography.titleMedium, color = colors.textPrimary ) Text( text = "Просыпайтесь под любимое радио", style = MaterialTheme.typography.labelMedium, color = colors.textSecondary ) } Icon( Lucide.ChevronRight, contentDescription = null, tint = colors.textMuted, modifier = Modifier.size(18.dp) ) } } // --- Таймер сна --- item { SectionLabel("ТАЙМЕР СНА") Spacer(Modifier.height(8.dp)) Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) .padding(horizontal = 16.dp, vertical = 12.dp) ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Отключить через", style = MaterialTheme.typography.titleMedium, color = colors.textPrimary ) Text( text = "$sleepTimer мин", style = MaterialTheme.typography.titleMedium, color = colors.accent, fontWeight = FontWeight.SemiBold ) } Slider( value = sleepTimer.toFloat(), onValueChange = { viewModel.setSleepTimer(it.toInt()) }, valueRange = 5f..120f, steps = 22, colors = SliderDefaults.colors( thumbColor = colors.accent, activeTrackColor = colors.accent, inactiveTrackColor = colors.surface2 ) ) } } // --- Качество звука по умолчанию --- item { SectionLabel("КАЧЕСТВО ЗВУКА") Spacer(Modifier.height(8.dp)) val options = listOf(0 to "Авто", 64 to "Эконом", 128 to "Стандарт", 320 to "Высокое") Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) .padding(12.dp), horizontalArrangement = Arrangement.spacedBy(6.dp) ) { options.forEach { (bitrate, label) -> val selected = preferredBitrate == bitrate val bgColor by animateColorAsState( targetValue = if (selected) colors.accent else colors.surface2, animationSpec = tween(Motion.Medium), label = "qSegment" ) val textColor by animateColorAsState( targetValue = if (selected) colors.bgBase else colors.textSecondary, animationSpec = tween(Motion.Medium), label = "qText" ) Box( modifier = Modifier .weight(1f) .clip(RoundedCornerShape(10.dp)) .background(bgColor) .clickable { viewModel.setPreferredBitrate(bitrate) } .padding(vertical = 10.dp), contentAlignment = Alignment.Center ) { Text( text = label, style = MaterialTheme.typography.labelLarge, color = textColor, fontWeight = FontWeight.Medium ) } } } Text( text = "Применяется к станциям с несколькими потоками. «Авто» — выбор станции.", style = MaterialTheme.typography.bodySmall, color = colors.textMuted, modifier = Modifier.padding(top = 8.dp) ) } // --- Эквалайзер (отдельный детальный экран) --- item { SectionLabel("ЗВУК") Spacer(Modifier.height(8.dp)) Row( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) .clickable { onNavigateToEqualizer() } .padding(horizontal = 16.dp, vertical = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(14.dp) ) { Icon( Lucide.SlidersHorizontal, contentDescription = null, tint = colors.accent, modifier = Modifier.size(22.dp) ) Column(modifier = Modifier.weight(1f)) { Text( text = "Эквалайзер", style = MaterialTheme.typography.titleMedium, color = colors.textPrimary ) Text( text = "Полосы, пресеты, бас, объём, громкость", style = MaterialTheme.typography.labelMedium, color = colors.textSecondary ) } Icon( Lucide.ChevronRight, contentDescription = null, tint = colors.textMuted, modifier = Modifier.size(18.dp) ) } } // --- Стиль визуализации воспроизведения --- item { SectionLabel("АНИМАЦИЯ ВОСПРОИЗВЕДЕНИЯ") Spacer(Modifier.height(8.dp)) Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { com.radiola.ui.components.VisualizerStyle.entries.chunked(2).forEach { rowStyles -> Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { rowStyles.forEach { style -> val selected = visualizerStyle == style.key Column( modifier = Modifier .weight(1f) .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border( width = if (selected) 2.dp else 1.dp, color = if (selected) colors.accent else colors.border, shape = RoundedCornerShape(16.dp) ) .clickable { viewModel.setVisualizerStyle(style.key) } .padding(12.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { com.radiola.ui.components.Visualizer( style = style, levels = null, playing = true, color = colors.accent, modifier = Modifier .fillMaxWidth() .height(48.dp) ) Text( text = style.label, style = MaterialTheme.typography.labelLarge, color = if (selected) colors.accent else colors.textSecondary, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal ) } } } } } } // --- Музыкальные сервисы --- item { SectionLabel("МУЗЫКАЛЬНЫЕ СЕРВИСЫ") Spacer(Modifier.height(8.dp)) Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) ) { // В store-сборке скрываем SOVA (сторонний мод ВК) — только sideload. val services = DeeplinkService.entries.filter { com.radiola.BuildConfig.SHOW_DEV_TOOLS || it != DeeplinkService.SOVA } services.forEachIndexed { index, service -> val checked = service.serviceId in enabledServices val trackColor by animateColorAsState( targetValue = if (checked) colors.accent else colors.surface2, animationSpec = tween(Motion.Medium), label = "switchTrack" ) Row( modifier = Modifier .fillMaxWidth() .clickable { viewModel.toggleService(service.serviceId, !checked) } .padding(horizontal = 16.dp, vertical = 14.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { Text( text = service.displayName, style = MaterialTheme.typography.titleMedium, color = colors.textPrimary ) Switch( checked = checked, onCheckedChange = { viewModel.toggleService(service.serviceId, it) }, colors = SwitchDefaults.colors( checkedThumbColor = colors.bgBase, checkedTrackColor = colors.accent, uncheckedThumbColor = colors.textMuted, uncheckedTrackColor = colors.surface2 ) ) } if (index < services.size - 1) { HorizontalDivider( color = colors.border, modifier = Modifier.padding(horizontal = 16.dp) ) } } } } // --- Тестирование станций (dev-инструмент, только в sideload) --- if (com.radiola.BuildConfig.SHOW_DEV_TOOLS) item { SectionLabel("ТЕСТИРОВАНИЕ СТАНЦИЙ") Spacer(Modifier.height(8.dp)) Column( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(16.dp)) .background(colors.surface) .border(1.dp, colors.border, RoundedCornerShape(16.dp)) .padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { if (isTesting) { Text( text = "Проверено $testProgress из $testTotal", style = MaterialTheme.typography.bodyMedium, color = colors.textSecondary ) LinearProgressIndicator( progress = { if (testTotal > 0) testProgress.toFloat() / testTotal else 0f }, modifier = Modifier.fillMaxWidth(), color = colors.accent, trackColor = colors.surface2 ) } else if (testResults.isNotEmpty()) { val ok = testResults.count { it.status == StationTestStatus.OK } val okNoMeta = testResults.count { it.status == StationTestStatus.OK_NO_META } val offline = testResults.count { it.status == StationTestStatus.OFFLINE } val error = testResults.count { it.status == StationTestStatus.ERROR } Text("Всего: ${testResults.size}", color = colors.textPrimary, style = MaterialTheme.typography.bodyMedium) Text("Работают + метаданные: $ok", color = Color(0xFF4CAF50), style = MaterialTheme.typography.bodyMedium) Text("Работают без метаданных: $okNoMeta", color = Color(0xFFFF9800), style = MaterialTheme.typography.bodyMedium) Text("Оффлайн: $offline", color = colors.live, style = MaterialTheme.typography.bodyMedium) Text("Ошибки: $error", color = colors.live, style = MaterialTheme.typography.bodyMedium) Spacer(Modifier.height(4.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Button( onClick = { showReport = true }, colors = ButtonDefaults.buttonColors( containerColor = colors.accent, contentColor = colors.bgBase ) ) { Text("Подробный отчёт") } OutlinedButton( onClick = { viewModel.clearTestResults() }, colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.textSecondary), border = androidx.compose.foundation.BorderStroke(1.dp, colors.border) ) { Text("Очистить") } } } else { OutlinedButton( onClick = { viewModel.startTesting() }, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.accent), border = androidx.compose.foundation.BorderStroke(1.dp, colors.accent) ) { Text("Провести тестирование") } } } } } // Диалог отчёта if (showReport) { AlertDialog( onDismissRequest = { showReport = false }, containerColor = colors.elevated, title = { Text( "Результаты тестирования", color = colors.textPrimary, style = MaterialTheme.typography.titleLarge ) }, text = { LazyColumn(modifier = Modifier.heightIn(max = 400.dp)) { items(testResults) { result -> val color = when (result.status) { StationTestStatus.OK -> Color(0xFF4CAF50) StationTestStatus.OK_NO_META -> Color(0xFFFF9800) StationTestStatus.OFFLINE -> colors.live StationTestStatus.ERROR -> colors.live } Column(modifier = Modifier.padding(vertical = 4.dp)) { Text( text = result.stationName, style = MaterialTheme.typography.bodyMedium, color = color ) Text( text = buildString { append("${result.status.name}") result.httpCode?.let { append(" | HTTP $it") } result.icyTitle?.let { append(" | Icy: $it") } result.nowPlayingTrack?.let { append(" | NP: $it") } result.errorMessage?.let { append(" | $it") } }, style = MaterialTheme.typography.labelSmall, color = colors.textMuted ) } } } }, confirmButton = { TextButton( onClick = { showReport = false }, colors = ButtonDefaults.textButtonColors(contentColor = colors.accent) ) { Text("Закрыть") } } ) } } /** * Превью цветовой темы: квадрат с фоном палитры, акцентным кружком и брендовым * градиентом снизу. Выбранная — с акцентной рамкой и жирной подписью. */ @Composable private fun ThemeSwatch( palette: ThemePalette, selected: Boolean, onClick: () -> Unit ) { val p = palette.colors val outer = RadiolaTheme.colors Column( modifier = Modifier .width(72.dp) .clip(RoundedCornerShape(18.dp)) .clickable(onClick = onClick), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(6.dp) ) { Box( modifier = Modifier .size(64.dp) .clip(RoundedCornerShape(18.dp)) .background(p.bgBase) .border( width = if (selected) 2.dp else 1.dp, color = if (selected) outer.accent else outer.border, shape = RoundedCornerShape(18.dp) ) ) { Box( modifier = Modifier .align(Alignment.Center) .size(26.dp) .clip(CircleShape) .background(p.accent) ) Box( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() .height(10.dp) .background(p.brandGradient) ) } Text( text = palette.title, style = MaterialTheme.typography.labelMedium, color = if (selected) outer.accent else outer.textSecondary, fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal ) } } /** Подпись секции: заглавные буквы, textMuted, labelSmall. */ @Composable private fun SectionLabel(text: String) { Text( text = text, style = MaterialTheme.typography.labelSmall, color = RadiolaTheme.colors.textMuted, letterSpacing = 1.sp ) }