Browse Source

Replace stream rotation menu with three marked toggle buttons

master
evgeny 6 days ago
parent
commit
8bee8c10ff
  1. 83
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt

83
tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/StreamScreen.kt

@ -9,25 +9,34 @@ import android.view.TextureView
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
@ -166,10 +175,13 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
}, label = { Text("${camera.lens.label} ${camera.lens.mm}".trim()) })
}
}
val modes = cameras.firstOrNull { it.id == mode?.camera }?.modes.orEmpty()
val camera = cameras.firstOrNull { it.id == mode?.camera }
val modes = camera?.modes.orEmpty()
val context = LocalContext.current
val configuration = LocalConfiguration.current
val display = remember(configuration) { ((context as? Activity)?.windowManager?.defaultDisplay?.rotation ?: 0) * 90 }
var expanded by remember { mutableStateOf(false) }
var orientationsExpanded by remember { mutableStateOf(false) }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) {
Box {
OutlinedButton(onClick = { expanded = true }, enabled = enabled && modes.isNotEmpty()) { Text(mode?.label.orEmpty()) }
DropdownMenu(expanded = expanded && enabled, onDismissRequest = { expanded = false }) {
@ -178,16 +190,45 @@ private fun CaptureOptions(cameras: List<StreamCamera>, config: StreamCaptureCon
}) }
}
}
Box {
OutlinedButton(onClick = { orientationsExpanded = true }, enabled = enabled && config != null) {
Text(stringResource(R.string.stream_orientation, config?.orientation?.degrees ?: 0))
for (option in listOf(StreamOrientation.UPRIGHT, StreamOrientation.RIGHT, StreamOrientation.LEFT)) {
val active = config?.orientation == option
val label = stringResource(R.string.stream_orientation, option.degrees)
val top = if (camera?.lens?.type == LensType.SELFIE) (display - option.degrees + 360) % 360
else (option.degrees - display + 360) % 360
OutlinedButton(onClick = { config?.let { onChange(it.copy(orientation = option)) } }, enabled = enabled && config != null,
modifier = Modifier.size(44.dp).semantics { selected = active; contentDescription = label },
shape = RoundedCornerShape(6.dp), contentPadding = PaddingValues(2.dp),
border = BorderStroke(if (active) 2.dp else 1.dp,
if (active) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = if (active) MaterialTheme.colorScheme.primaryContainer else Color.Transparent)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
StreamRotationMark(top)
Text("${option.degrees}°", fontSize = 10.sp, lineHeight = 10.sp)
}
}
DropdownMenu(expanded = orientationsExpanded && enabled, onDismissRequest = { orientationsExpanded = false }) {
StreamOrientation.values().forEach { option -> DropdownMenuItem(text = { Text("${option.degrees}°") }, onClick = {
orientationsExpanded = false; config?.let { onChange(it.copy(orientation = option)) }
}) }
}
}
}
@Composable
private fun StreamRotationMark(top: Int) {
val frame = LocalContentColor.current
val marker = MaterialTheme.colorScheme.error
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
Canvas(Modifier.matchParentSize()) {
val left = size.width * 0.15f; val right = size.width * 0.85f
val upper = size.height * 0.08f; val lower = size.height * 0.92f
drawRoundRect(frame, Offset(left, upper), Size(right - left, lower - upper), CornerRadius(2.dp.toPx()), style = Stroke(1.dp.toPx()))
val edge = when (top) {
90 -> Offset(right, upper) to Offset(right, lower)
180 -> Offset(left, lower) to Offset(right, lower)
270 -> Offset(left, upper) to Offset(left, lower)
else -> Offset(left, upper) to Offset(right, upper)
}
drawLine(marker, edge.first, edge.second, 3.dp.toPx(), StrokeCap.Round)
}
Text(stringResource(R.string.stream_top), fontSize = 7.sp, lineHeight = 7.sp)
}
}
@ -215,7 +256,6 @@ private fun LiveStreamView(group: Long, source: Long, epoch: Long, localPreview:
private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPreview: StreamPreview?, displayDegrees: Int) {
val engine = remember(group, source, epoch, localPreview) { StreamPlayerEngine(group, source, epoch, localPreview, displayDegrees) }
val aspect by engine.aspect.collectAsState()
val top by engine.top.collectAsState()
val status by engine.status.collectAsState()
var surface by remember { mutableStateOf<Surface?>(null) }
DisposableEffect(engine) { onDispose { engine.stop() } }
@ -240,25 +280,8 @@ private fun LiveStreamSurface(group: Long, source: Long, epoch: Long, localPrevi
}, modifier = Modifier.matchParentSize(), onRelease = { view ->
engine.stop(); view.surfaceTextureListener = null; surface?.release(); surface = null
})
top?.let { StreamTopMarker(it) }
}
}
}
}
@Composable
private fun BoxScope.StreamTopMarker(direction: Int) {
val alignment = when (direction) {
90 -> Alignment.CenterEnd
180 -> Alignment.BottomCenter
270 -> Alignment.CenterStart
else -> Alignment.TopCenter
}
Surface(modifier = Modifier.align(alignment).padding(8.dp), shape = RoundedCornerShape(6.dp), color = Color.Black.copy(alpha = 0.65f)) {
Row(Modifier.padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Icon(Icons.Filled.ArrowUpward, contentDescription = null, tint = Color.White, modifier = Modifier.size(16.dp).rotate(direction.toFloat()))
Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.stream_top), color = Color.White, style = MaterialTheme.typography.bodySmall)
}
}
}

Loading…
Cancel
Save