feat(stations): свайп по списку листает чипы + свечение играющей станции
1) Горизонтальный свайп по области списка переключает фильтры-чипы в их порядке ([Все]+жанры), выбранный чип автоскроллится в зону видимости. Вертикальная прокрутка грида сохраняется. 2) У играющей станции в списке — мягкое радиальное свечение позади обложки, которое «гуляет» (двигается центр) и вылезает из-под краёв, + эквалайзер- бейдж в углу. Источник активной станции — PlayerController.currentStationId. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.radiola.domain.usecase.GetStationsUseCase
|
||||
import com.radiola.domain.usecase.PlayStationUseCase
|
||||
import com.radiola.domain.usecase.RefreshStationsUseCase
|
||||
import com.radiola.domain.usecase.ToggleFavoriteUseCase
|
||||
import com.radiola.service.PlayerController
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -25,9 +26,14 @@ class StationsViewModel @Inject constructor(
|
||||
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
|
||||
private val favoritesRepository: FavoritesRepository,
|
||||
private val stationRepository: StationRepository,
|
||||
private val nowPlayingRepository: NowPlayingRepository
|
||||
private val nowPlayingRepository: NowPlayingRepository,
|
||||
private val playerController: PlayerController
|
||||
) : ViewModel() {
|
||||
|
||||
// Активная (играющая) станция — для подсветки карточки в списке.
|
||||
val playingStationId: StateFlow<Int?> = playerController.currentStationId
|
||||
val isPlaying: StateFlow<Boolean> = playerController.isPlaying
|
||||
|
||||
private val _searchQuery = MutableStateFlow("")
|
||||
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user