feat(nav+fav): порядок меню, анимация иконки при выборе, индикатор на избранном
- Порядок нижнего меню: Радио · Избранное · История · Чарты · Запись · Настройки. - Иконка вкладки при выборе делает упругий scale-«поп» (spring MediumBouncy) — в нижнем баре и боковом рейле. - На экране «Избранное» играющая станция теперь подсвечивается так же, как на главной: вращающееся свечение под обложкой + индикатор-эквалайзер в углу (FavoritesViewModel отдаёт playingStationId/isPlaying из PlayerController, FavoritesScreen передаёт isCurrent/isPlaying в StationCard).
This commit is contained in:
@@ -35,6 +35,8 @@ fun FavoritesScreen(
|
|||||||
val favorites by viewModel.favorites.collectAsState()
|
val favorites by viewModel.favorites.collectAsState()
|
||||||
val favoriteIds by viewModel.favoriteIds.collectAsState()
|
val favoriteIds by viewModel.favoriteIds.collectAsState()
|
||||||
val nowPlaying by viewModel.nowPlaying.collectAsState()
|
val nowPlaying by viewModel.nowPlaying.collectAsState()
|
||||||
|
val playingStationId by viewModel.playingStationId.collectAsState()
|
||||||
|
val isPlaying by viewModel.isPlaying.collectAsState()
|
||||||
val colors = RadiolaTheme.colors
|
val colors = RadiolaTheme.colors
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
@@ -95,6 +97,8 @@ fun FavoritesScreen(
|
|||||||
onClick = { onStationClick(station) },
|
onClick = { onStationClick(station) },
|
||||||
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
||||||
nowTrack = nowPlaying[station.id],
|
nowTrack = nowPlaying[station.id],
|
||||||
|
isCurrent = station.id == playingStationId,
|
||||||
|
isPlaying = isPlaying,
|
||||||
modifier = Modifier.animateItemPlacement()
|
modifier = Modifier.animateItemPlacement()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.radiola.domain.repository.NowPlayingRepository
|
|||||||
import com.radiola.domain.usecase.ToggleFavoriteUseCase
|
import com.radiola.domain.usecase.ToggleFavoriteUseCase
|
||||||
import com.radiola.domain.usecase.auth.PushFavoriteUseCase
|
import com.radiola.domain.usecase.auth.PushFavoriteUseCase
|
||||||
import com.radiola.domain.usecase.auth.SyncFavoritesUseCase
|
import com.radiola.domain.usecase.auth.SyncFavoritesUseCase
|
||||||
|
import com.radiola.service.PlayerController
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
@@ -24,9 +25,14 @@ class FavoritesViewModel @Inject constructor(
|
|||||||
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
|
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
|
||||||
private val pushFavoriteUseCase: PushFavoriteUseCase,
|
private val pushFavoriteUseCase: PushFavoriteUseCase,
|
||||||
private val syncFavoritesUseCase: SyncFavoritesUseCase,
|
private val syncFavoritesUseCase: SyncFavoritesUseCase,
|
||||||
private val nowPlayingRepository: NowPlayingRepository
|
private val nowPlayingRepository: NowPlayingRepository,
|
||||||
|
private val playerController: PlayerController
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
// Активная (играющая) станция — для подсветки карточки, как на экране всех станций.
|
||||||
|
val playingStationId: StateFlow<Int?> = playerController.currentStationId
|
||||||
|
val isPlaying: StateFlow<Boolean> = playerController.isPlaying
|
||||||
|
|
||||||
val favorites: StateFlow<List<Station>> = favoritesRepository.getFavorites()
|
val favorites: StateFlow<List<Station>> = favoritesRepository.getFavorites()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.radiola.ui.navigation
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColorAsState
|
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.core.tween
|
||||||
import androidx.compose.animation.expandHorizontally
|
import androidx.compose.animation.expandHorizontally
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
@@ -28,11 +31,13 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
@@ -43,9 +48,9 @@ import com.radiola.ui.theme.RadiolaTheme
|
|||||||
// при холодном старте может содержать null (порядок инициализации Kotlin).
|
// при холодном старте может содержать null (порядок инициализации Kotlin).
|
||||||
private val navItems = listOf(
|
private val navItems = listOf(
|
||||||
NavDestinations.Stations,
|
NavDestinations.Stations,
|
||||||
NavDestinations.Charts,
|
|
||||||
NavDestinations.Favorites,
|
NavDestinations.Favorites,
|
||||||
NavDestinations.History,
|
NavDestinations.History,
|
||||||
|
NavDestinations.Charts,
|
||||||
NavDestinations.Recordings,
|
NavDestinations.Recordings,
|
||||||
NavDestinations.Settings
|
NavDestinations.Settings
|
||||||
)
|
)
|
||||||
@@ -144,6 +149,13 @@ private fun VerticalPillTab(
|
|||||||
animationSpec = tween(Motion.Medium),
|
animationSpec = tween(Motion.Medium),
|
||||||
label = "railTabFg"
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(52.dp)
|
.size(52.dp)
|
||||||
@@ -161,7 +173,7 @@ private fun VerticalPillTab(
|
|||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
tint = content,
|
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),
|
animationSpec = tween(Motion.Medium),
|
||||||
label = "tabFg"
|
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(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -204,7 +224,7 @@ private fun PillTab(
|
|||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = label,
|
contentDescription = label,
|
||||||
tint = content,
|
tint = content,
|
||||||
modifier = Modifier.height(22.dp).width(22.dp)
|
modifier = Modifier.height(22.dp).width(22.dp).scale(pop.value)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user