Compare commits
4 Commits
44ea21042f
...
a3f3494da2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3f3494da2 | ||
|
|
8a951dd4c5 | ||
|
|
58f735823e | ||
|
|
9e9f4c8009 |
@@ -9,9 +9,11 @@ import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.radiola.data.local.TokenDataStore
|
||||
@@ -65,23 +67,30 @@ class MainActivity : ComponentActivity() {
|
||||
val isRecording by playerViewModel.isRecording.collectAsState()
|
||||
val isLoggedIn by tokenDataStore.isLoggedIn.collectAsState(initial = false)
|
||||
|
||||
val startDestination = remember(isLoggedIn) {
|
||||
if (isLoggedIn) NavDestinations.Stations.route else NavDestinations.Auth.route
|
||||
}
|
||||
// Авторизация необязательна — всегда стартуем со станций.
|
||||
// Вход доступен из Настроек.
|
||||
val startDestination = NavDestinations.Stations.route
|
||||
|
||||
val currentRoute = navController
|
||||
.currentBackStackEntryAsState().value?.destination?.route
|
||||
val showChrome = currentRoute != NavDestinations.Auth.route
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
Column {
|
||||
if (currentStation != null) {
|
||||
MiniPlayer(
|
||||
stationName = currentStation!!.name,
|
||||
track = currentTrack,
|
||||
isPlaying = isPlaying,
|
||||
onClick = { showPlayer = true },
|
||||
onPlayPause = { playerViewModel.togglePlayPause() }
|
||||
)
|
||||
}
|
||||
if (isLoggedIn) {
|
||||
if (showChrome) {
|
||||
Column(Modifier.navigationBarsPadding()) {
|
||||
if (currentStation != null) {
|
||||
MiniPlayer(
|
||||
stationName = currentStation!!.name,
|
||||
track = currentTrack,
|
||||
isPlaying = isPlaying,
|
||||
onClick = { showPlayer = true },
|
||||
onPlayPause = { playerViewModel.togglePlayPause() }
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
// Навигация доступна и без входа — приложением можно
|
||||
// пользоваться анонимно.
|
||||
BottomNavBar(navController)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.radiola.domain.model.Track
|
||||
import com.radiola.domain.repository.NowPlayingRepository
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import javax.inject.Inject
|
||||
|
||||
class NowPlayingRepositoryImpl @Inject constructor(
|
||||
@@ -21,11 +21,19 @@ class NowPlayingRepositoryImpl @Inject constructor(
|
||||
socketClient.connect()
|
||||
}
|
||||
|
||||
// Объединяем два источника: сокет (реалтайм, приоритет) и REST-поллинг
|
||||
// (refreshNowPlaying). Раньше REST-данные писались в _nowPlaying, но никем
|
||||
// не читались — из-за этого трек и обложка не отображались.
|
||||
override fun getNowPlaying(stationId: Int): Flow<Track?> {
|
||||
return socketClient.nowPlaying.map { it[stationId] }
|
||||
return combine(socketClient.nowPlaying, _nowPlaying) { socketMap, restMap ->
|
||||
socketMap[stationId] ?: restMap[stationId]
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAllNowPlaying(): Flow<Map<Int, Track>> = socketClient.nowPlaying
|
||||
override fun getAllNowPlaying(): Flow<Map<Int, Track>> =
|
||||
combine(socketClient.nowPlaying, _nowPlaying) { socketMap, restMap ->
|
||||
restMap + socketMap
|
||||
}
|
||||
|
||||
override suspend fun refreshNowPlaying(): Result<Unit> {
|
||||
return try {
|
||||
|
||||
16
app/src/main/java/com/radiola/ui/components/Images.kt
Normal file
16
app/src/main/java/com/radiola/ui/components/Images.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.radiola.ui.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import coil.request.ImageRequest
|
||||
|
||||
/**
|
||||
* Модель изображения с плавным проявлением (crossfade).
|
||||
* Используется во всех обложках, чтобы загрузка не «моргала».
|
||||
*/
|
||||
@Composable
|
||||
fun crossfadeModel(url: String?): ImageRequest =
|
||||
ImageRequest.Builder(LocalContext.current)
|
||||
.data(url)
|
||||
.crossfade(220)
|
||||
.build()
|
||||
@@ -2,6 +2,10 @@ package com.radiola.ui.components
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -29,6 +33,7 @@ import com.radiola.ui.theme.Motion
|
||||
import com.radiola.ui.theme.RadiolaTheme
|
||||
import com.radiola.ui.theme.pressScale
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MiniPlayer(
|
||||
stationName: String,
|
||||
@@ -39,6 +44,7 @@ fun MiniPlayer(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val haptics = LocalHapticFeedback.current
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -59,7 +65,7 @@ fun MiniPlayer(
|
||||
) {
|
||||
if (track?.coverUrl != null) {
|
||||
AsyncImage(
|
||||
model = track.coverUrl,
|
||||
model = crossfadeModel(track.coverUrl),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
@@ -83,7 +89,7 @@ fun MiniPlayer(
|
||||
style = androidx.compose.material3.MaterialTheme.typography.titleMedium,
|
||||
color = colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis
|
||||
modifier = Modifier.basicMarquee()
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
@@ -95,9 +101,11 @@ fun MiniPlayer(
|
||||
.background(colors.accent)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onPlayPause
|
||||
),
|
||||
indication = null
|
||||
) {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onPlayPause()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Crossfade(targetState = isPlaying, animationSpec = tween(Motion.Fast), label = "miniPlay") { playing ->
|
||||
|
||||
@@ -15,7 +15,9 @@ 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.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
@@ -36,6 +38,7 @@ fun StationCard(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val haptics = LocalHapticFeedback.current
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
val heartTint by animateColorAsState(
|
||||
targetValue = if (isFavorite) colors.accent else colors.textPrimary,
|
||||
@@ -57,7 +60,7 @@ fun StationCard(
|
||||
) {
|
||||
if (!station.coverUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = station.coverUrl,
|
||||
model = crossfadeModel(station.coverUrl),
|
||||
contentDescription = station.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
@@ -77,7 +80,10 @@ fun StationCard(
|
||||
.size(32.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.background(androidx.compose.ui.graphics.Color.Black.copy(alpha = 0.4f))
|
||||
.clickable(onClick = onFavoriteClick),
|
||||
.clickable {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onFavoriteClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
|
||||
@@ -51,7 +51,7 @@ fun TrackListItem(
|
||||
) {
|
||||
if (!track.coverUrl.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = track.coverUrl,
|
||||
model = crossfadeModel(track.coverUrl),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
|
||||
@@ -37,7 +37,15 @@ import com.radiola.ui.theme.RadiolaTheme
|
||||
fun BottomNavBar(navController: NavController) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||
val items = NavDestinations.items.filter { it.showInBottomBar }
|
||||
// Обращаемся к объектам напрямую: companion-список NavDestinations.items
|
||||
// при холодном старте может содержать null (порядок инициализации Kotlin).
|
||||
val items = listOf(
|
||||
NavDestinations.Stations,
|
||||
NavDestinations.Favorites,
|
||||
NavDestinations.History,
|
||||
NavDestinations.Recordings,
|
||||
NavDestinations.Settings
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.radiola.ui.player
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
@@ -8,6 +10,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
@@ -19,7 +22,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -46,6 +51,7 @@ import com.radiola.ui.theme.Motion
|
||||
import com.radiola.ui.theme.RadiolaTheme
|
||||
import com.radiola.ui.theme.pressScale
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PlayerBottomSheet(
|
||||
station: Station?,
|
||||
@@ -64,11 +70,13 @@ fun PlayerBottomSheet(
|
||||
val context = LocalContext.current
|
||||
val enabledServices by viewModel.enabledServices.collectAsState()
|
||||
val colors = RadiolaTheme.colors
|
||||
val haptics = LocalHapticFeedback.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.bgBase)
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
@@ -93,7 +101,7 @@ fun PlayerBottomSheet(
|
||||
val coverModel = track?.coverUrl ?: station?.coverUrl
|
||||
if (!coverModel.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = coverModel,
|
||||
model = com.radiola.ui.components.crossfadeModel(coverModel),
|
||||
contentDescription = station?.name,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
@@ -121,8 +129,8 @@ fun PlayerBottomSheet(
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
color = colors.textPrimary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
modifier = Modifier.basicMarquee()
|
||||
)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(
|
||||
@@ -159,7 +167,13 @@ fun PlayerBottomSheet(
|
||||
label = "heartTint"
|
||||
)
|
||||
PlayerIconBtn(size = 44.dp) {
|
||||
IconButton(onClick = onToggleFavorite, modifier = Modifier.size(44.dp)) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onToggleFavorite()
|
||||
},
|
||||
modifier = Modifier.size(44.dp)
|
||||
) {
|
||||
Icon(Lucide.Heart, "Избранное", tint = heartTint, modifier = Modifier.size(22.dp))
|
||||
}
|
||||
}
|
||||
@@ -179,7 +193,10 @@ fun PlayerBottomSheet(
|
||||
.clip(CircleShape)
|
||||
.background(colors.accent)
|
||||
.pressScale(interactionSource = playInteraction)
|
||||
.clickable(interactionSource = playInteraction, indication = null, onClick = onPlayPause),
|
||||
.clickable(interactionSource = playInteraction, indication = null) {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onPlayPause()
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Crossfade(
|
||||
@@ -262,7 +279,20 @@ private fun PlayerIconBtn(
|
||||
}
|
||||
}
|
||||
|
||||
/** Монохромная кнопка сервиса для поиска трека (без официальных логотипов). */
|
||||
/** Короткая подпись сервиса под кнопкой (без обрезки слов). */
|
||||
private fun serviceShortName(service: DeeplinkService): String = when (service.serviceId) {
|
||||
"yandex" -> "Яндекс"
|
||||
"vk" -> "ВК Музыка"
|
||||
"boom" -> "BOOM"
|
||||
"spotify" -> "Spotify"
|
||||
"apple" -> "Apple Music"
|
||||
"youtube" -> "YT Music"
|
||||
"tidal" -> "Tidal"
|
||||
"deezer" -> "Deezer"
|
||||
else -> service.displayName
|
||||
}
|
||||
|
||||
/** Монохромная кнопка сервиса для поиска трека. */
|
||||
@Composable
|
||||
private fun ServiceDeeplinkBtn(
|
||||
service: DeeplinkService,
|
||||
@@ -302,7 +332,7 @@ private fun ServiceDeeplinkBtn(
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = service.displayName.take(8),
|
||||
text = serviceShortName(service),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = colors.textSecondary,
|
||||
maxLines = 1,
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.radiola.ui.stations
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.*
|
||||
@@ -18,6 +14,8 @@ 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
|
||||
@@ -38,11 +36,7 @@ fun StationsScreen(
|
||||
val favoriteIds by viewModel.favoriteIds.collectAsState()
|
||||
val colors = RadiolaTheme.colors
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
// Двухцветный заголовок экрана
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
@@ -50,72 +44,82 @@ fun StationsScreen(
|
||||
withStyle(SpanStyle(color = colors.accent)) { append("радио") }
|
||||
},
|
||||
style = MaterialTheme.typography.headlineLarge,
|
||||
modifier = Modifier.padding(top = 20.dp, bottom = 16.dp)
|
||||
modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 16.dp)
|
||||
)
|
||||
|
||||
when {
|
||||
isLoading && stations.isEmpty() -> Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(color = colors.accent)
|
||||
}
|
||||
// Поиск — всегда виден (в т.ч. когда результатов нет)
|
||||
SearchBar(
|
||||
query = searchQuery,
|
||||
onQueryChange = viewModel::onSearchQueryChange,
|
||||
modifier = Modifier.padding(horizontal = 20.dp)
|
||||
)
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
stations.isEmpty() -> {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn() + slideInVertically()
|
||||
) {
|
||||
// Жанры — всегда видны
|
||||
if (tags.isNotEmpty()) {
|
||||
FilterChips(
|
||||
tags = tags,
|
||||
selectedTag = selectedTag,
|
||||
onTagSelected = viewModel::onTagSelected
|
||||
)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
// Область результатов — единственная прокручиваемая зона
|
||||
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
|
||||
when {
|
||||
isLoading && stations.isEmpty() -> {
|
||||
CircularProgressIndicator(
|
||||
color = colors.accent,
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
stations.isEmpty() -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
EmptyState(message = error ?: "Станции не найдены")
|
||||
if (selectedTag != null) {
|
||||
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.onTagSelected(null) },
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = colors.accent
|
||||
),
|
||||
onClick = {
|
||||
viewModel.onSearchQueryChange("")
|
||||
viewModel.onTagSelected(null)
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.accent),
|
||||
border = androidx.compose.foundation.BorderStroke(1.dp, colors.accent)
|
||||
) {
|
||||
Text("Показать все")
|
||||
Text("Сбросить фильтры")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
SearchBar(
|
||||
query = searchQuery,
|
||||
onQueryChange = viewModel::onSearchQueryChange,
|
||||
)
|
||||
}
|
||||
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||
FilterChips(
|
||||
tags = tags,
|
||||
selectedTag = selectedTag,
|
||||
onTagSelected = viewModel::onTagSelected,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
items(stations, key = { it.id }) { station ->
|
||||
StationCard(
|
||||
station = station,
|
||||
isFavorite = favoriteIds.contains(station.id),
|
||||
onClick = { onStationClick(station) },
|
||||
onFavoriteClick = { viewModel.toggleFavorite(station) },
|
||||
modifier = Modifier.animateItemPlacement()
|
||||
)
|
||||
else -> LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.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) },
|
||||
modifier = Modifier.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user