Add landscape layout support for main player screen
Refactors PlayerContent into reusable components and adds orientation detection. In landscape mode, artwork appears on the left with station info and controls on the right for better horizontal space usage.
This commit is contained in:
parent
f9adf3d3db
commit
44c84ab57f
@ -1,5 +1,6 @@
|
||||
package cz.bugsy.karemote.ui.screens.main
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -45,6 +46,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@ -226,6 +228,39 @@ private fun PlayerContent(
|
||||
onNextStation: () -> Unit,
|
||||
onPreviousStation: () -> Unit,
|
||||
onShowStationPicker: () -> Unit
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
|
||||
if (isLandscape) {
|
||||
LandscapePlayerContent(
|
||||
uiState = uiState,
|
||||
onTogglePlayPause = onTogglePlayPause,
|
||||
onVolumeChange = onVolumeChange,
|
||||
onNextStation = onNextStation,
|
||||
onPreviousStation = onPreviousStation,
|
||||
onShowStationPicker = onShowStationPicker
|
||||
)
|
||||
} else {
|
||||
PortraitPlayerContent(
|
||||
uiState = uiState,
|
||||
onTogglePlayPause = onTogglePlayPause,
|
||||
onVolumeChange = onVolumeChange,
|
||||
onNextStation = onNextStation,
|
||||
onPreviousStation = onPreviousStation,
|
||||
onShowStationPicker = onShowStationPicker
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortraitPlayerContent(
|
||||
uiState: MainUiState,
|
||||
onTogglePlayPause: () -> Unit,
|
||||
onVolumeChange: (Int) -> Unit,
|
||||
onNextStation: () -> Unit,
|
||||
onPreviousStation: () -> Unit,
|
||||
onShowStationPicker: () -> Unit
|
||||
) {
|
||||
val status = uiState.karadioStatus
|
||||
val radioBrowserInfo = uiState.radioBrowserInfo
|
||||
@ -236,45 +271,149 @@ private fun PlayerContent(
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Station artwork
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(200.dp)
|
||||
.clickable { onShowStationPicker() },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (!radioBrowserInfo?.favicon.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = radioBrowserInfo?.favicon,
|
||||
contentDescription = "Station logo",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Radio,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(80.dp),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StationArtwork(
|
||||
favicon = radioBrowserInfo?.favicon,
|
||||
size = 200.dp,
|
||||
onShowStationPicker = onShowStationPicker
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
StationInfo(
|
||||
status = status,
|
||||
radioBrowserInfo = radioBrowserInfo,
|
||||
onShowStationPicker = onShowStationPicker
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
PlaybackControls(
|
||||
isPlaying = status.isPlaying,
|
||||
onTogglePlayPause = onTogglePlayPause,
|
||||
onNextStation = onNextStation,
|
||||
onPreviousStation = onPreviousStation
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
VolumeControl(
|
||||
volume = status.volume,
|
||||
onVolumeChange = onVolumeChange
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LandscapePlayerContent(
|
||||
uiState: MainUiState,
|
||||
onTogglePlayPause: () -> Unit,
|
||||
onVolumeChange: (Int) -> Unit,
|
||||
onNextStation: () -> Unit,
|
||||
onPreviousStation: () -> Unit,
|
||||
onShowStationPicker: () -> Unit
|
||||
) {
|
||||
val status = uiState.karadioStatus
|
||||
val radioBrowserInfo = uiState.radioBrowserInfo
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Left side: Artwork
|
||||
StationArtwork(
|
||||
favicon = radioBrowserInfo?.favicon,
|
||||
size = 180.dp,
|
||||
onShowStationPicker = onShowStationPicker
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
|
||||
// Right side: Info and controls
|
||||
Column(
|
||||
modifier = Modifier.weight(1f),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
StationInfo(
|
||||
status = status,
|
||||
radioBrowserInfo = radioBrowserInfo,
|
||||
onShowStationPicker = onShowStationPicker
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
PlaybackControls(
|
||||
isPlaying = status.isPlaying,
|
||||
onTogglePlayPause = onTogglePlayPause,
|
||||
onNextStation = onNextStation,
|
||||
onPreviousStation = onPreviousStation
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
VolumeControl(
|
||||
volume = status.volume,
|
||||
onVolumeChange = onVolumeChange
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StationArtwork(
|
||||
favicon: String?,
|
||||
size: androidx.compose.ui.unit.Dp,
|
||||
onShowStationPicker: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.clickable { onShowStationPicker() },
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (!favicon.isNullOrBlank()) {
|
||||
AsyncImage(
|
||||
model = favicon,
|
||||
contentDescription = "Station logo",
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.primaryContainer),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Radio,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(size / 2.5f),
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StationInfo(
|
||||
status: cz.bugsy.karemote.data.model.KaradioStatus,
|
||||
radioBrowserInfo: cz.bugsy.karemote.data.model.RadioBrowserStation?,
|
||||
onShowStationPicker: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Station name
|
||||
Text(
|
||||
text = status.stationName.ifBlank { "No Station" },
|
||||
@ -341,103 +480,108 @@ private fun PlayerContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
// Playback controls
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
FilledIconButton(
|
||||
onClick = onPreviousStation,
|
||||
modifier = Modifier.size(56.dp),
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SkipPrevious,
|
||||
contentDescription = "Previous station",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
FilledIconButton(
|
||||
onClick = onTogglePlayPause,
|
||||
modifier = Modifier.size(72.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (status.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||
contentDescription = if (status.isPlaying) "Pause" else "Play",
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
FilledIconButton(
|
||||
onClick = onNextStation,
|
||||
modifier = Modifier.size(56.dp),
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SkipNext,
|
||||
contentDescription = "Next station",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
|
||||
// Volume control
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
@Composable
|
||||
private fun PlaybackControls(
|
||||
isPlaying: Boolean,
|
||||
onTogglePlayPause: () -> Unit,
|
||||
onNextStation: () -> Unit,
|
||||
onPreviousStation: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
FilledIconButton(
|
||||
onClick = onPreviousStation,
|
||||
modifier = Modifier.size(56.dp),
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.VolumeUp,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Volume",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${(status.volume * 100 / 254)}%",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Slider(
|
||||
value = status.volume.toFloat(),
|
||||
onValueChange = { onVolumeChange(it.toInt()) },
|
||||
valueRange = 0f..254f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
imageVector = Icons.Default.SkipPrevious,
|
||||
contentDescription = "Previous station",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
FilledIconButton(
|
||||
onClick = onTogglePlayPause,
|
||||
modifier = Modifier.size(72.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
|
||||
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
FilledIconButton(
|
||||
onClick = onNextStation,
|
||||
modifier = Modifier.size(56.dp),
|
||||
colors = IconButtonDefaults.filledIconButtonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.SkipNext,
|
||||
contentDescription = "Next station",
|
||||
modifier = Modifier.size(32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VolumeControl(
|
||||
volume: Int,
|
||||
onVolumeChange: (Int) -> Unit
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.VolumeUp,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Volume",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
text = "${(volume * 100 / 254)}%",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Slider(
|
||||
value = volume.toFloat(),
|
||||
onValueChange = { onVolumeChange(it.toInt()) },
|
||||
valueRange = 0f..254f,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user