feat(orientation): полноценная поддержка альбомной ориентации
- боковой nav-rail слева вместо нижнего бара в альбоме (SideNavRail) - мини-плеер уезжает под контент в альбомной раскладке - плеер эфира: двухпанельный (обложка слева, инфо/эквалайзер/контролы справа) - плеер записи: слева управление, справа прокручиваемый список треков - сетки станций и избранного: 4 колонки в альбоме вместо 2 - хелпер isLandscape() через LocalConfiguration Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -82,7 +82,7 @@ fun FavoritesScreen(
|
||||
}
|
||||
} else {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
columns = GridCells.Fixed(if (com.radiola.ui.util.isLandscape()) 4 else 2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(bottom = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
|
||||
@@ -12,13 +12,19 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -33,20 +39,33 @@ import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import com.radiola.ui.theme.Motion
|
||||
import com.radiola.ui.theme.RadiolaTheme
|
||||
|
||||
// Обращаемся к объектам напрямую: companion-список NavDestinations.items
|
||||
// при холодном старте может содержать null (порядок инициализации Kotlin).
|
||||
private val navItems = listOf(
|
||||
NavDestinations.Stations,
|
||||
NavDestinations.Charts,
|
||||
NavDestinations.Favorites,
|
||||
NavDestinations.History,
|
||||
NavDestinations.Recordings,
|
||||
NavDestinations.Settings
|
||||
)
|
||||
|
||||
/** Переход на раздел с сохранением состояния (общий для нижнего бара и бокового рейла). */
|
||||
private fun NavController.navigateToTab(route: String, currentRoute: String?) {
|
||||
if (currentRoute != route) {
|
||||
navigate(route) {
|
||||
popUpTo(graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomNavBar(navController: NavController) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||
// Обращаемся к объектам напрямую: companion-список NavDestinations.items
|
||||
// при холодном старте может содержать null (порядок инициализации Kotlin).
|
||||
val items = listOf(
|
||||
NavDestinations.Stations,
|
||||
NavDestinations.Charts,
|
||||
NavDestinations.Favorites,
|
||||
NavDestinations.History,
|
||||
NavDestinations.Recordings,
|
||||
NavDestinations.Settings
|
||||
)
|
||||
val items = navItems
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -67,20 +86,86 @@ fun BottomNavBar(navController: NavController) {
|
||||
icon = destination.icon,
|
||||
selected = selected,
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
if (currentRoute != destination.route) {
|
||||
navController.navigate(destination.route) {
|
||||
popUpTo(navController.graph.startDestinationId) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
}
|
||||
onClick = { navController.navigateToTab(destination.route, currentRoute) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Боковой навигационный рейл для альбомной ориентации.
|
||||
* Вертикальная капсула с теми же иконками-вкладками, что и нижний бар.
|
||||
*/
|
||||
@Composable
|
||||
fun SideNavRail(navController: NavController) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.statusBarsPadding()
|
||||
.padding(start = 12.dp, end = 4.dp, top = 12.dp, bottom = 12.dp)
|
||||
.width(64.dp)
|
||||
.clip(RoundedCornerShape(32.dp))
|
||||
.background(colors.surface2)
|
||||
.border(1.dp, colors.border, RoundedCornerShape(32.dp))
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
navItems.forEach { destination ->
|
||||
VerticalPillTab(
|
||||
label = destination.labelRes,
|
||||
icon = destination.icon,
|
||||
selected = currentRoute == destination.route,
|
||||
onClick = { navController.navigateToTab(destination.route, currentRoute) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerticalPillTab(
|
||||
label: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val colors = RadiolaTheme.colors
|
||||
val bg by animateColorAsState(
|
||||
targetValue = if (selected) colors.accent else androidx.compose.ui.graphics.Color.Transparent,
|
||||
animationSpec = tween(Motion.Medium),
|
||||
label = "railTabBg"
|
||||
)
|
||||
val content by animateColorAsState(
|
||||
targetValue = if (selected) colors.bgBase else colors.textSecondary,
|
||||
animationSpec = tween(Motion.Medium),
|
||||
label = "railTabFg"
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.size(52.dp)
|
||||
.clip(RoundedCornerShape(26.dp))
|
||||
.background(bg)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null,
|
||||
onClick = onClick
|
||||
),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = label,
|
||||
tint = content,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PillTab(
|
||||
label: String,
|
||||
|
||||
@@ -86,17 +86,12 @@ fun PlayerBottomSheet(
|
||||
val spectrum by viewModel.spectrum.collectAsState()
|
||||
val vizStyle by viewModel.visualizerStyle.collectAsState()
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.bgBase)
|
||||
.navigationBarsPadding()
|
||||
// Скролл — чтобы на телефонах с меньшей высотой в dp (высокий dpi)
|
||||
// низ плеера (кнопка «Текст песни») не обрезался шторкой.
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
val landscape = com.radiola.ui.util.isLandscape()
|
||||
|
||||
// ── Секции плеера как лямбды: переиспользуются в портретной (колонка)
|
||||
// и альбомной (две панели) раскладках. ──
|
||||
|
||||
val labelSection: @Composable () -> Unit = {
|
||||
// Метка «В ЭФИРЕ» + чип качества справа (если у станции есть варианты)
|
||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
@@ -118,8 +113,9 @@ fun PlayerBottomSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
}
|
||||
|
||||
val nameSection: @Composable () -> Unit = {
|
||||
// Название радиостанции — под меткой, над обложкой
|
||||
Text(
|
||||
text = station?.name ?: "",
|
||||
@@ -131,12 +127,13 @@ fun PlayerBottomSheet(
|
||||
textAlign = androidx.compose.ui.text.style.TextAlign.Center,
|
||||
modifier = Modifier.basicMarquee()
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
val coverSection: @Composable (Dp) -> Unit = { coverSize ->
|
||||
// Обложка станции/трека
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(190.dp)
|
||||
.size(coverSize)
|
||||
.clip(RoundedCornerShape(24.dp))
|
||||
.background(colors.surface2),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -155,8 +152,9 @@ fun PlayerBottomSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(14.dp))
|
||||
}
|
||||
|
||||
val trackInfoSection: @Composable () -> Unit = {
|
||||
// Название трека и исполнитель с Crossfade при смене
|
||||
Crossfade(
|
||||
targetState = track?.song to track?.artist,
|
||||
@@ -183,8 +181,9 @@ fun PlayerBottomSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
val visualizerSection: @Composable () -> Unit = {
|
||||
// Живой эквалайзер — вместо прогресс-бара (эфир нельзя перематывать)
|
||||
com.radiola.ui.components.Visualizer(
|
||||
style = com.radiola.ui.components.VisualizerStyle.fromKey(vizStyle),
|
||||
@@ -195,8 +194,9 @@ fun PlayerBottomSheet(
|
||||
.fillMaxWidth()
|
||||
.height(if (com.radiola.ui.components.VisualizerStyle.fromKey(vizStyle) == com.radiola.ui.components.VisualizerStyle.RADIAL) 120.dp else 40.dp)
|
||||
)
|
||||
Spacer(Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
val controlsSection: @Composable () -> Unit = {
|
||||
// Управление воспроизведением
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
||||
@@ -286,8 +286,9 @@ fun PlayerBottomSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
val servicesSection: @Composable () -> Unit = {
|
||||
// Ряд кнопок музыкальных сервисов
|
||||
if (enabledServices.isNotEmpty()) {
|
||||
LazyRow(
|
||||
@@ -307,16 +308,16 @@ fun PlayerBottomSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val lyricsSection: @Composable () -> Unit = {
|
||||
// Кнопка «Текст песни» — активна только когда играет трек.
|
||||
// Явная пилюля с фоном: на реальном телефоне мелкий TextButton почти не виден.
|
||||
if (track != null) {
|
||||
val lyricsInteraction = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(colors.surface2)
|
||||
.pressScale(interactionSource = lyricsInteraction)
|
||||
@@ -343,6 +344,77 @@ fun PlayerBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
if (landscape) {
|
||||
// Альбом: слева обложка с названием станции, справа — трек, эквалайзер,
|
||||
// управление и сервисы (правая панель скроллится на низких экранах).
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.bgBase)
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(0.42f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
labelSection()
|
||||
Spacer(Modifier.height(6.dp))
|
||||
nameSection()
|
||||
Spacer(Modifier.height(14.dp))
|
||||
coverSection(170.dp)
|
||||
}
|
||||
Spacer(Modifier.width(24.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(0.58f)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
trackInfoSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
visualizerSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
controlsSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
servicesSection()
|
||||
if (track != null) {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
lyricsSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(colors.bgBase)
|
||||
.navigationBarsPadding()
|
||||
// Скролл — чтобы на телефонах с меньшей высотой в dp (высокий dpi)
|
||||
// низ плеера (кнопка «Текст песни») не обрезался шторкой.
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp, vertical = 20.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
labelSection()
|
||||
Spacer(Modifier.height(6.dp))
|
||||
nameSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
coverSection(190.dp)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
trackInfoSection()
|
||||
Spacer(Modifier.height(20.dp))
|
||||
visualizerSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
controlsSection()
|
||||
Spacer(Modifier.height(20.dp))
|
||||
servicesSection()
|
||||
if (enabledServices.isNotEmpty()) Spacer(Modifier.height(12.dp))
|
||||
lyricsSection()
|
||||
}
|
||||
}
|
||||
|
||||
// Шторка выбора качества
|
||||
if (showQuality && station != null) {
|
||||
val qualities = station.qualities
|
||||
|
||||
@@ -56,18 +56,10 @@ fun RecordingPlayerSheet(
|
||||
viewModel.play(recording)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// Отступ под системную навигацию — иначе список треков уходит под кнопки
|
||||
.navigationBarsPadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
val landscape = com.radiola.ui.util.isLandscape()
|
||||
val effectiveDuration = durationMs.coerceAtLeast(recording.duration ?: 1L).coerceAtLeast(1L)
|
||||
|
||||
val headerSection: @Composable () -> Unit = {
|
||||
// Метка «ЗАПИСЬ ЭФИРА»
|
||||
Text(
|
||||
text = "ЗАПИСЬ ЭФИРА",
|
||||
@@ -77,9 +69,7 @@ fun RecordingPlayerSheet(
|
||||
),
|
||||
color = colors.accent
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
// Название станции
|
||||
Text(
|
||||
text = recording.stationName,
|
||||
@@ -88,9 +78,7 @@ fun RecordingPlayerSheet(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
// Трек (если есть) + дата
|
||||
val meta = buildString {
|
||||
if (!recording.trackName.isNullOrBlank()) {
|
||||
@@ -106,11 +94,10 @@ fun RecordingPlayerSheet(
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
val seekSection: @Composable () -> Unit = {
|
||||
// Seekbar
|
||||
val effectiveDuration = durationMs.coerceAtLeast(recording.duration ?: 1L).coerceAtLeast(1L)
|
||||
Slider(
|
||||
value = positionMs.toFloat(),
|
||||
onValueChange = { viewModel.seekTo(it.toLong()) },
|
||||
@@ -122,7 +109,6 @@ fun RecordingPlayerSheet(
|
||||
inactiveTrackColor = colors.surface2
|
||||
)
|
||||
)
|
||||
|
||||
// Время: текущее и общее
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -139,9 +125,9 @@ fun RecordingPlayerSheet(
|
||||
color = colors.textMuted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
val controlsSection: @Composable () -> Unit = {
|
||||
// Ряд управления: rewind15, play/pause, forward15
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -210,40 +196,96 @@ fun RecordingPlayerSheet(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Список треков записи с переходом по тайм-коду
|
||||
// Заголовок + строки списка треков. modifier — чтобы в альбоме правая
|
||||
// панель скроллилась отдельно.
|
||||
val markersSection: @Composable (Modifier) -> Unit = { listModifier ->
|
||||
if (recording.markers.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
Column(modifier = listModifier) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Треки в записи",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = colors.textPrimary,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "${recording.markers.size}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = colors.textMuted
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// Индекс текущего трека: последняя метка, до которой уже дошло время
|
||||
val activeIndex = recording.markers.indexOfLast { positionMs >= it.offsetMs }
|
||||
recording.markers.forEachIndexed { index, marker ->
|
||||
MarkerRow(
|
||||
timecode = formatMs(marker.offsetMs),
|
||||
title = marker.title,
|
||||
active = index == activeIndex,
|
||||
onClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
viewModel.seekTo(marker.offsetMs)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (landscape) {
|
||||
// Альбом: слева управление, справа — прокручиваемый список треков.
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 8.dp, bottom = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.weight(0.5f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "Треки в записи",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = colors.textPrimary,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Text(
|
||||
text = "${recording.markers.size}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = colors.textMuted
|
||||
headerSection()
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
seekSection()
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
controlsSection()
|
||||
}
|
||||
if (recording.markers.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
markersSection(
|
||||
Modifier
|
||||
.weight(0.5f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// Отступ под системную навигацию — иначе список треков уходит под кнопки
|
||||
.navigationBarsPadding()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
// Индекс текущего трека: последняя метка, до которой уже дошло время
|
||||
val activeIndex = recording.markers.indexOfLast { positionMs >= it.offsetMs }
|
||||
recording.markers.forEachIndexed { index, marker ->
|
||||
MarkerRow(
|
||||
timecode = formatMs(marker.offsetMs),
|
||||
title = marker.title,
|
||||
active = index == activeIndex,
|
||||
onClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
viewModel.seekTo(marker.offsetMs)
|
||||
}
|
||||
)
|
||||
headerSection()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
seekSection()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
controlsSection()
|
||||
if (recording.markers.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(28.dp))
|
||||
markersSection(Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ fun StationsScreen(
|
||||
val isPlaying by viewModel.isPlaying.collectAsState()
|
||||
val colors = RadiolaTheme.colors
|
||||
val haptics = LocalHapticFeedback.current
|
||||
// В альбоме шире окно — больше колонок, иначе карточки растягиваются.
|
||||
val gridColumns = if (com.radiola.ui.util.isLandscape()) 4 else 2
|
||||
|
||||
// Полный порядок фильтров: «Все» (null) + жанры. Свайп листает по нему.
|
||||
val orderedTags = remember(tags) { listOf<String?>(null) + tags }
|
||||
@@ -136,7 +138,7 @@ fun StationsScreen(
|
||||
}
|
||||
|
||||
else -> LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
columns = GridCells.Fixed(gridColumns),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
// top = высота чипов: грид уходит ПОД них, свечение верхнего ряда
|
||||
// не обрезается и проступает за чипами.
|
||||
|
||||
16
app/src/main/java/com/radiola/ui/util/Orientation.kt
Normal file
16
app/src/main/java/com/radiola/ui/util/Orientation.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.radiola.ui.util
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
|
||||
/**
|
||||
* Альбомная ориентация (ширина больше высоты).
|
||||
* Используется для адаптивной раскладки: боковой nav-rail вместо нижнего бара,
|
||||
* двухпанельный плеер, больше колонок в сетках станций.
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun isLandscape(): Boolean =
|
||||
LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
Reference in New Issue
Block a user