feat(orientation): полноценная поддержка альбомной ориентации
- боковой nav-rail слева вместо нижнего бара в альбоме (SideNavRail) - мини-плеер уезжает под контент в альбомной раскладке - плеер эфира: двухпанельный (обложка слева, инфо/эквалайзер/контролы справа) - плеер записи: слева управление, справа прокручиваемый список треков - сетки станций и избранного: 4 колонки в альбоме вместо 2 - хелпер isLandscape() через LocalConfiguration Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user