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
|
package cz.bugsy.karemote.ui.screens.main
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -45,6 +46,7 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -226,6 +228,39 @@ private fun PlayerContent(
|
|||||||
onNextStation: () -> Unit,
|
onNextStation: () -> Unit,
|
||||||
onPreviousStation: () -> Unit,
|
onPreviousStation: () -> Unit,
|
||||||
onShowStationPicker: () -> 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 status = uiState.karadioStatus
|
||||||
val radioBrowserInfo = uiState.radioBrowserInfo
|
val radioBrowserInfo = uiState.radioBrowserInfo
|
||||||
@ -236,45 +271,149 @@ private fun PlayerContent(
|
|||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Station artwork
|
StationArtwork(
|
||||||
Card(
|
favicon = radioBrowserInfo?.favicon,
|
||||||
modifier = Modifier
|
size = 200.dp,
|
||||||
.size(200.dp)
|
onShowStationPicker = onShowStationPicker
|
||||||
.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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
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
|
// Station name
|
||||||
Text(
|
Text(
|
||||||
text = status.stationName.ifBlank { "No Station" },
|
text = status.stationName.ifBlank { "No Station" },
|
||||||
@ -341,103 +480,108 @@ private fun PlayerContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
@Composable
|
||||||
|
private fun PlaybackControls(
|
||||||
// Playback controls
|
isPlaying: Boolean,
|
||||||
Row(
|
onTogglePlayPause: () -> Unit,
|
||||||
horizontalArrangement = Arrangement.Center,
|
onNextStation: () -> Unit,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onPreviousStation: () -> Unit
|
||||||
modifier = Modifier.fillMaxWidth()
|
) {
|
||||||
) {
|
Row(
|
||||||
FilledIconButton(
|
horizontalArrangement = Arrangement.Center,
|
||||||
onClick = onPreviousStation,
|
verticalAlignment = Alignment.CenterVertically
|
||||||
modifier = Modifier.size(56.dp),
|
) {
|
||||||
colors = IconButtonDefaults.filledIconButtonColors(
|
FilledIconButton(
|
||||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
onClick = onPreviousStation,
|
||||||
)
|
modifier = Modifier.size(56.dp),
|
||||||
) {
|
colors = IconButtonDefaults.filledIconButtonColors(
|
||||||
Icon(
|
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
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
|
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Column(
|
Icon(
|
||||||
modifier = Modifier.padding(16.dp)
|
imageVector = Icons.Default.SkipPrevious,
|
||||||
) {
|
contentDescription = "Previous station",
|
||||||
Row(
|
modifier = Modifier.size(32.dp)
|
||||||
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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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