package com.radiola.ui.stations import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Radio import com.radiola.domain.model.Station import com.radiola.ui.components.* import com.radiola.ui.theme.RadiolaTheme @OptIn(ExperimentalFoundationApi::class) @Composable fun StationsScreen( onStationClick: (Station) -> Unit, modifier: Modifier = Modifier, viewModel: StationsViewModel = hiltViewModel() ) { val stations by viewModel.stations.collectAsState() val tags by viewModel.tags.collectAsState() val searchQuery by viewModel.searchQuery.collectAsState() val selectedTag by viewModel.selectedTag.collectAsState() val isLoading by viewModel.isLoading.collectAsState() val error by viewModel.error.collectAsState() val favoriteIds by viewModel.favoriteIds.collectAsState() val nowPlaying by viewModel.nowPlaying.collectAsState() val playingStationId by viewModel.playingStationId.collectAsState() val isPlaying by viewModel.isPlaying.collectAsState() val colors = RadiolaTheme.colors val haptics = LocalHapticFeedback.current // В альбоме шире окно — больше колонок, иначе карточки растягиваются. val gridColumns = if (com.radiola.ui.util.isLandscape()) 4 else 2 // Полный порядок фильтров: «Все» (null) + жанры. Свайп листает по нему. val orderedTags = remember(tags) { listOf(null) + tags } fun switchTag(forward: Boolean) { if (orderedTags.size <= 1) return val idx = orderedTags.indexOf(selectedTag).coerceAtLeast(0) val newIdx = idx + if (forward) 1 else -1 if (newIdx in orderedTags.indices) { haptics.performHapticFeedback(HapticFeedbackType.LongPress) viewModel.onTagSelected(orderedTags[newIdx]) } } Column(modifier = modifier.fillMaxSize()) { // Двухцветный заголовок экрана Text( text = buildAnnotatedString { withStyle(SpanStyle(color = colors.textPrimary)) { append("Выберите ") } withStyle(SpanStyle(color = colors.accent)) { append("радиостанцию") } }, style = MaterialTheme.typography.headlineLarge, modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 16.dp) ) // Поиск — всегда виден (в т.ч. когда результатов нет) SearchBar( query = searchQuery, onQueryChange = viewModel::onSearchQueryChange, modifier = Modifier.padding(horizontal = 20.dp) ) Spacer(Modifier.height(12.dp)) // Область результатов — единственная прокручиваемая зона. // Горизонтальный свайп листает фильтры-чипы (вертикаль остаётся у грида). Box( modifier = Modifier .weight(1f) .fillMaxWidth() .pointerInput(orderedTags, selectedTag) { var totalDx = 0f detectHorizontalDragGestures( onDragStart = { totalDx = 0f }, onDragEnd = { val threshold = 56.dp.toPx() when { totalDx <= -threshold -> switchTag(forward = true) totalDx >= threshold -> switchTag(forward = false) } } ) { _, dragAmount -> totalDx += dragAmount } } ) { when { isLoading && stations.isEmpty() -> { CircularProgressIndicator( color = colors.accent, modifier = Modifier.align(Alignment.Center) ) } stations.isEmpty() -> { Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { EmptyState( message = error ?: if (searchQuery.isNotBlank() || selectedTag != null) "Ничего не найдено" else "Станции не найдены", icon = Lucide.Radio, modifier = Modifier.wrapContentSize() ) if (searchQuery.isNotBlank() || selectedTag != null) { Spacer(Modifier.height(12.dp)) OutlinedButton( onClick = { viewModel.onSearchQueryChange("") viewModel.onTagSelected(null) }, colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.accent), border = androidx.compose.foundation.BorderStroke(1.dp, colors.accent) ) { Text("Сбросить фильтры") } } } } else -> LazyVerticalGrid( columns = GridCells.Fixed(gridColumns), modifier = Modifier.fillMaxSize(), // top = высота чипов: грид уходит ПОД них, свечение верхнего ряда // не обрезается и проступает за чипами. contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 54.dp, bottom = 20.dp), horizontalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp) ) { items(stations, key = { it.id }) { station -> StationCard( station = station, isFavorite = favoriteIds.contains(station.id), onClick = { onStationClick(station) }, onFavoriteClick = { viewModel.toggleFavorite(station) }, nowTrack = nowPlaying[station.id], isCurrent = station.id == playingStationId, isPlaying = isPlaying, modifier = Modifier.animateItemPlacement() ) } } } // Чипы-фильтры поверх грида. Фон-градиент: вверху непрозрачный // (маскирует прокручиваемые карточки), книзу прозрачный — свечение // верхнего ряда станций проступает ИЗ-ПОД чипов. if (tags.isNotEmpty()) { Box( modifier = Modifier .align(Alignment.TopStart) .fillMaxWidth() .background( Brush.verticalGradient( 0f to colors.bgBase, 0.55f to colors.bgBase, 1f to Color.Transparent ) ) .padding(top = 2.dp, bottom = 12.dp) ) { Box(modifier = Modifier.fillMaxWidth().height(44.dp)) { // Чипы во всю ширину, но с отступом слева под кнопку; у левого // края — затухание прозрачности (чипы «уплывают» под кнопку). FilterChips( tags = tags, selectedTag = selectedTag, onTagSelected = viewModel::onTagSelected, contentPadding = PaddingValues(start = 66.dp, end = 16.dp), modifier = Modifier .fillMaxWidth() .align(Alignment.Center) .fadingStartEdge(60.dp) ) // Кнопка-категории — поверх чипов, слева. CategoryPicker( title = "Категории", items = tags, selected = selectedTag, onSelect = viewModel::onTagSelected, modifier = Modifier.align(Alignment.CenterStart).padding(start = 16.dp) ) } } } } } }