feat(stations): свайп по списку листает чипы + свечение играющей станции

1) Горизонтальный свайп по области списка переключает фильтры-чипы в их
   порядке ([Все]+жанры), выбранный чип автоскроллится в зону видимости.
   Вертикальная прокрутка грида сохраняется.

2) У играющей станции в списке — мягкое радиальное свечение позади обложки,
   которое «гуляет» (двигается центр) и вылезает из-под краёв, + эквалайзер-
   бейдж в углу. Источник активной станции — PlayerController.currentStationId.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 14:12:01 +03:00
parent 603e232dff
commit 9268e14cc6
6 changed files with 169 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
package com.radiola.ui.stations
import androidx.compose.foundation.ExperimentalFoundationApi
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
@@ -9,6 +10,9 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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
@@ -35,7 +39,22 @@ fun StationsScreen(
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
// Полный порядок фильтров: «Все» (null) + жанры. Свайп листает по нему.
val orderedTags = remember(tags) { listOf<String?>(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()) {
// Двухцветный заголовок экрана
@@ -66,8 +85,26 @@ fun StationsScreen(
Spacer(Modifier.height(8.dp))
}
// Область результатов — единственная прокручиваемая зона
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
// Область результатов — единственная прокручиваемая зона.
// Горизонтальный свайп листает фильтры-чипы (вертикаль остаётся у грида).
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(
@@ -119,6 +156,8 @@ fun StationsScreen(
onClick = { onStationClick(station) },
onFavoriteClick = { viewModel.toggleFavorite(station) },
nowTrack = nowPlaying[station.id],
isCurrent = station.id == playingStationId,
isPlaying = isPlaying,
modifier = Modifier.animateItemPlacement()
)
}