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:
Pavel Baksy 2025-11-23 00:00:15 +01:00
parent db9796bf29
commit f5ca6d2bdc

View File

@ -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,10 +271,106 @@ private fun PlayerContent(
) { ) {
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Station artwork 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( Card(
modifier = Modifier modifier = Modifier
.size(200.dp) .size(size)
.clickable { onShowStationPicker() }, .clickable { onShowStationPicker() },
shape = RoundedCornerShape(16.dp), shape = RoundedCornerShape(16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
@ -248,9 +379,9 @@ private fun PlayerContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
if (!radioBrowserInfo?.favicon.isNullOrBlank()) { if (!favicon.isNullOrBlank()) {
AsyncImage( AsyncImage(
model = radioBrowserInfo?.favicon, model = favicon,
contentDescription = "Station logo", contentDescription = "Station logo",
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop contentScale = ContentScale.Crop
@ -265,16 +396,24 @@ private fun PlayerContent(
Icon( Icon(
imageVector = Icons.Default.Radio, imageVector = Icons.Default.Radio,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(80.dp), modifier = Modifier.size(size / 2.5f),
tint = MaterialTheme.colorScheme.onPrimaryContainer tint = MaterialTheme.colorScheme.onPrimaryContainer
) )
} }
} }
} }
} }
}
Spacer(modifier = Modifier.height(24.dp)) @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,14 +480,19 @@ private fun PlayerContent(
} }
} }
} }
}
}
Spacer(modifier = Modifier.weight(1f)) @Composable
private fun PlaybackControls(
// Playback controls isPlaying: Boolean,
onTogglePlayPause: () -> Unit,
onNextStation: () -> Unit,
onPreviousStation: () -> Unit
) {
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth()
) { ) {
FilledIconButton( FilledIconButton(
onClick = onPreviousStation, onClick = onPreviousStation,
@ -371,8 +515,8 @@ private fun PlayerContent(
modifier = Modifier.size(72.dp) modifier = Modifier.size(72.dp)
) { ) {
Icon( Icon(
imageVector = if (status.isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow, imageVector = if (isPlaying) Icons.Default.Pause else Icons.Default.PlayArrow,
contentDescription = if (status.isPlaying) "Pause" else "Play", contentDescription = if (isPlaying) "Pause" else "Play",
modifier = Modifier.size(40.dp) modifier = Modifier.size(40.dp)
) )
} }
@ -393,10 +537,13 @@ private fun PlayerContent(
) )
} }
} }
}
Spacer(modifier = Modifier.height(32.dp)) @Composable
private fun VolumeControl(
// Volume control volume: Int,
onVolumeChange: (Int) -> Unit
) {
Card( Card(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors( colors = CardDefaults.cardColors(
@ -422,23 +569,20 @@ private fun PlayerContent(
) )
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
Text( Text(
text = "${(status.volume * 100 / 254)}%", text = "${(volume * 100 / 254)}%",
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Slider( Slider(
value = status.volume.toFloat(), value = volume.toFloat(),
onValueChange = { onVolumeChange(it.toInt()) }, onValueChange = { onVolumeChange(it.toInt()) },
valueRange = 0f..254f, valueRange = 0f..254f,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
} }
} }
Spacer(modifier = Modifier.height(16.dp))
}
} }
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)