feat(nav+fav): порядок меню, анимация иконки при выборе, индикатор на избранном

- Порядок нижнего меню: Радио · Избранное · История · Чарты · Запись · Настройки.
- Иконка вкладки при выборе делает упругий scale-«поп» (spring MediumBouncy) —
  в нижнем баре и боковом рейле.
- На экране «Избранное» играющая станция теперь подсвечивается так же, как на
  главной: вращающееся свечение под обложкой + индикатор-эквалайзер в углу
  (FavoritesViewModel отдаёт playingStationId/isPlaying из PlayerController,
  FavoritesScreen передаёт isCurrent/isPlaying в StationCard).
This commit is contained in:
nk
2026-06-07 17:06:28 +03:00
parent d63c1d4187
commit a5d9a06c3f
3 changed files with 34 additions and 4 deletions

View File

@@ -35,6 +35,8 @@ fun FavoritesScreen(
val favorites by viewModel.favorites.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
Column(
@@ -95,6 +97,8 @@ fun FavoritesScreen(
onClick = { onStationClick(station) },
onFavoriteClick = { viewModel.toggleFavorite(station) },
nowTrack = nowPlaying[station.id],
isCurrent = station.id == playingStationId,
isPlaying = isPlaying,
modifier = Modifier.animateItemPlacement()
)
}

View File

@@ -9,6 +9,7 @@ import com.radiola.domain.repository.NowPlayingRepository
import com.radiola.domain.usecase.ToggleFavoriteUseCase
import com.radiola.domain.usecase.auth.PushFavoriteUseCase
import com.radiola.domain.usecase.auth.SyncFavoritesUseCase
import com.radiola.service.PlayerController
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
@@ -24,9 +25,14 @@ class FavoritesViewModel @Inject constructor(
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
private val pushFavoriteUseCase: PushFavoriteUseCase,
private val syncFavoritesUseCase: SyncFavoritesUseCase,
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
val favorites: StateFlow<List<Station>> = favoritesRepository.getFavorites()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())

View File

@@ -2,6 +2,9 @@ package com.radiola.ui.navigation
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandHorizontally
import androidx.compose.animation.fadeIn
@@ -28,11 +31,13 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
@@ -43,9 +48,9 @@ import com.radiola.ui.theme.RadiolaTheme
// при холодном старте может содержать null (порядок инициализации Kotlin).
private val navItems = listOf(
NavDestinations.Stations,
NavDestinations.Charts,
NavDestinations.Favorites,
NavDestinations.History,
NavDestinations.Charts,
NavDestinations.Recordings,
NavDestinations.Settings
)
@@ -144,6 +149,13 @@ private fun VerticalPillTab(
animationSpec = tween(Motion.Medium),
label = "railTabFg"
)
val pop = remember { Animatable(1f) }
LaunchedEffect(selected) {
if (selected) {
pop.snapTo(0.5f)
pop.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = 620f))
}
}
Row(
modifier = Modifier
.size(52.dp)
@@ -161,7 +173,7 @@ private fun VerticalPillTab(
imageVector = icon,
contentDescription = label,
tint = content,
modifier = Modifier.size(22.dp)
modifier = Modifier.size(22.dp).scale(pop.value)
)
}
}
@@ -185,6 +197,14 @@ private fun PillTab(
animationSpec = tween(Motion.Medium),
label = "tabFg"
)
// Упругий «поп» иконки при выборе вкладки — маленькая приятная деталь.
val pop = remember { Animatable(1f) }
LaunchedEffect(selected) {
if (selected) {
pop.snapTo(0.5f)
pop.animateTo(1f, spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = 620f))
}
}
Row(
modifier = modifier
.fillMaxWidth()
@@ -204,7 +224,7 @@ private fun PillTab(
imageVector = icon,
contentDescription = label,
tint = content,
modifier = Modifier.height(22.dp).width(22.dp)
modifier = Modifier.height(22.dp).width(22.dp).scale(pop.value)
)
}
}