feat(orientation): полноценная поддержка альбомной ориентации

- боковой nav-rail слева вместо нижнего бара в альбоме (SideNavRail)
- мини-плеер уезжает под контент в альбомной раскладке
- плеер эфира: двухпанельный (обложка слева, инфо/эквалайзер/контролы справа)
- плеер записи: слева управление, справа прокручиваемый список треков
- сетки станций и избранного: 4 колонки в альбоме вместо 2
- хелпер isLandscape() через LocalConfiguration

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 20:19:47 +03:00
parent fabf780450
commit 06cb6c16f1
7 changed files with 335 additions and 92 deletions

View File

@@ -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