@@ -47,6 +47,7 @@ import androidx.compose.material.icons.filled.Bolt
4747import androidx.compose.material.icons.filled.Camera
4848import androidx.compose.material.icons.filled.CameraAlt
4949import androidx.compose.material.icons.filled.Cameraswitch
50+ import androidx.compose.material.icons.filled.Check
5051import androidx.compose.material.icons.filled.Collections
5152import androidx.compose.material.icons.filled.ContentCopy
5253import androidx.compose.material.icons.filled.Exposure
@@ -65,6 +66,7 @@ import androidx.compose.material.icons.filled.Videocam
6566import androidx.compose.material.icons.filled.Visibility
6667import androidx.compose.material.icons.filled.VisibilityOff
6768import androidx.compose.material.icons.filled.Wifi
69+ import androidx.compose.material.icons.filled.WifiOff
6870import androidx.compose.material.icons.filled.WbSunny
6971import androidx.compose.material.icons.filled.ZoomIn
7072import androidx.compose.material3.Button
@@ -243,6 +245,7 @@ fun CameraScreen(
243245 onNavigateToCapture = onNavigateToCapture,
244246 onNavigateToSettings = onNavigateToSettings,
245247 onCopyStreamUrl = { viewModel.copyStreamUrl() },
248+ onToggleServer = { viewModel.toggleServer() },
246249 onSelectLens = { viewModel.selectLens(it) },
247250 )
248251
@@ -294,6 +297,7 @@ private fun ImmersiveCameraView(
294297 onNavigateToCapture : () -> Unit ,
295298 onNavigateToSettings : () -> Unit ,
296299 onCopyStreamUrl : () -> Unit ,
300+ onToggleServer : () -> Unit ,
297301 onSelectLens : (Int ) -> Unit ,
298302) {
299303 Box (modifier = Modifier .fillMaxSize()) {
@@ -369,6 +373,8 @@ private fun ImmersiveCameraView(
369373 onNavigateToCapture = onNavigateToCapture,
370374 onNavigateToSettings = onNavigateToSettings,
371375 onToggleQuickSettings = onToggleQuickSettings,
376+ onCopyStreamUrl = onCopyStreamUrl,
377+ onToggleServer = onToggleServer,
372378 modifier = Modifier
373379 .align(Alignment .TopStart )
374380 .fillMaxWidth()
@@ -417,7 +423,6 @@ private fun ImmersiveCameraView(
417423 onStreamToggle = onStreamToggle,
418424 onCapture = onCapture,
419425 onRecord = onRecord,
420- onCopyStreamUrl = onCopyStreamUrl,
421426 onSelectLens = onSelectLens,
422427 onToggleQuickSettings = onToggleQuickSettings,
423428 onQuickSettingTap = onQuickSettingTap,
@@ -451,6 +456,8 @@ private fun CameraTopOverlay(
451456 onNavigateToCapture : () -> Unit ,
452457 onNavigateToSettings : () -> Unit ,
453458 onToggleQuickSettings : () -> Unit ,
459+ onCopyStreamUrl : () -> Unit ,
460+ onToggleServer : () -> Unit ,
454461 modifier : Modifier = Modifier ,
455462) {
456463 var menuExpanded by remember { mutableStateOf(false ) }
@@ -481,6 +488,11 @@ private fun CameraTopOverlay(
481488 horizontalArrangement = Arrangement .spacedBy(2 .dp),
482489 verticalAlignment = Alignment .CenterVertically
483490 ) {
491+ ServerStatusButton (
492+ streamStatus = streamStatus,
493+ onCopyUrl = onCopyStreamUrl,
494+ onToggleServer = onToggleServer,
495+ )
484496 CameraControlButton (
485497 icon = if (showPreview) Icons .Default .Visibility else Icons .Default .VisibilityOff ,
486498 contentDescription = if (showPreview) " Hide preview" else " Show preview" ,
@@ -609,7 +621,6 @@ private fun CameraBottomOverlay(
609621 onStreamToggle : () -> Unit ,
610622 onCapture : () -> Unit ,
611623 onRecord : () -> Unit ,
612- onCopyStreamUrl : () -> Unit ,
613624 onSelectLens : (Int ) -> Unit ,
614625 onToggleQuickSettings : () -> Unit ,
615626 onQuickSettingTap : (QuickSettingType ) -> Unit ,
@@ -637,15 +648,6 @@ private fun CameraBottomOverlay(
637648 )
638649 }
639650
640- if (streamStatus.isServerRunning && streamStatus.url.isNotBlank()) {
641- StreamInfoBar (
642- url = streamStatus.url,
643- isStreaming = streamStatus.isActive,
644- clientCount = streamStatus.clientCount,
645- onCopyUrl = onCopyStreamUrl
646- )
647- }
648-
649651 if (availableLenses.size > 1 ) {
650652 LensSelectorRow (
651653 lenses = availableLenses,
@@ -1393,66 +1395,131 @@ private fun RecordingIndicator(
13931395}
13941396
13951397@Composable
1396- private fun StreamInfoBar (
1397- url : String ,
1398- isStreaming : Boolean ,
1399- clientCount : Int ,
1398+ private fun ServerStatusButton (
1399+ streamStatus : com.raulshma.lenscast.camera.model.StreamStatus ,
14001400 onCopyUrl : () -> Unit ,
1401+ onToggleServer : () -> Unit ,
1402+ modifier : Modifier = Modifier ,
14011403) {
1402- Surface (
1403- modifier = Modifier .fillMaxWidth(),
1404- color = OverlayScrim ,
1405- shape = RoundedCornerShape (0 .dp)
1406- ) {
1407- Row (
1408- modifier = Modifier
1409- .fillMaxWidth()
1410- .padding(horizontal = 16 .dp, vertical = 8 .dp),
1411- verticalAlignment = Alignment .CenterVertically
1404+ var expanded by remember { mutableStateOf(false ) }
1405+
1406+ val iconTint by animateColorAsState(
1407+ targetValue = when {
1408+ streamStatus.isActive -> Color (0xFF4CAF50 )
1409+ streamStatus.isServerRunning -> MaterialTheme .colorScheme.primary
1410+ else -> Color .White .copy(alpha = 0.4f )
1411+ },
1412+ animationSpec = tween(300 ),
1413+ label = " server_status_tint"
1414+ )
1415+
1416+ Box (modifier = modifier) {
1417+ CameraControlButton (
1418+ icon = Icons .Default .Wifi ,
1419+ contentDescription = " Web server status" ,
1420+ onClick = { expanded = true },
1421+ tint = iconTint,
1422+ )
1423+ DropdownMenu (
1424+ expanded = expanded,
1425+ onDismissRequest = { expanded = false },
1426+ containerColor = MaterialTheme .colorScheme.surfaceContainerHigh,
1427+ shape = RoundedCornerShape (16 .dp),
14121428 ) {
1413- Icon (
1414- imageVector = Icons .Default .Wifi ,
1415- contentDescription = null ,
1416- modifier = Modifier .size(16 .dp),
1417- tint = MaterialTheme .colorScheme.primary
1429+ DropdownMenuItem (
1430+ text = {
1431+ Column {
1432+ Text (
1433+ " Web Server" ,
1434+ style = MaterialTheme .typography.labelLarge,
1435+ fontWeight = FontWeight .Bold ,
1436+ )
1437+ Spacer (modifier = Modifier .height(4 .dp))
1438+ if (streamStatus.url.isNotBlank()) {
1439+ Text (
1440+ streamStatus.url,
1441+ style = MaterialTheme .typography.bodySmall.copy(
1442+ fontFamily = FontFamily .Monospace
1443+ ),
1444+ maxLines = 1 ,
1445+ )
1446+ } else {
1447+ Text (
1448+ " Server offline" ,
1449+ style = MaterialTheme .typography.bodySmall,
1450+ color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.5f ),
1451+ )
1452+ }
1453+ Spacer (modifier = Modifier .height(2 .dp))
1454+ Text (
1455+ when {
1456+ streamStatus.clientCount > 0 -> " ${streamStatus.clientCount} viewer(s) connected"
1457+ streamStatus.isActive -> " Live stream active"
1458+ streamStatus.isServerRunning -> " Server ready"
1459+ else -> " Offline"
1460+ },
1461+ style = MaterialTheme .typography.labelSmall,
1462+ color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.6f ),
1463+ )
1464+ }
1465+ },
1466+ onClick = { expanded = false },
1467+ enabled = false ,
14181468 )
1419- Spacer (modifier = Modifier .size(8 .dp))
1420- Column (modifier = Modifier .weight(1f )) {
1421- Text (
1422- text = url,
1423- style = MaterialTheme .typography.labelSmall.copy(
1424- fontFamily = FontFamily .Monospace
1425- ),
1426- maxLines = 1 ,
1427- color = Color .White .copy(alpha = 0.9f )
1428- )
1429- Text (
1430- text = when {
1431- clientCount > 0 -> " $clientCount viewer(s)"
1432- isStreaming -> " Live stream active"
1433- else -> " Web server ready"
1469+ if (streamStatus.url.isNotBlank()) {
1470+ DropdownMenuItem (
1471+ text = {
1472+ Row (verticalAlignment = Alignment .CenterVertically ) {
1473+ Icon (
1474+ Icons .Default .ContentCopy ,
1475+ contentDescription = null ,
1476+ modifier = Modifier .size(18 .dp),
1477+ tint = MaterialTheme .colorScheme.onSurface,
1478+ )
1479+ Spacer (modifier = Modifier .width(12 .dp))
1480+ Text (" Copy URL" , style = MaterialTheme .typography.bodyMedium)
1481+ }
1482+ },
1483+ onClick = {
1484+ expanded = false
1485+ onCopyUrl()
14341486 },
1435- style = MaterialTheme .typography.labelSmall,
1436- color = Color .White .copy(alpha = 0.6f )
14371487 )
14381488 }
1439- Surface (
1440- color = OverlayLight ,
1441- shape = CircleShape ,
1442- onClick = onCopyUrl
1443- ) {
1444- Box (
1445- modifier = Modifier .size(32 .dp),
1446- contentAlignment = Alignment .Center
1447- ) {
1448- Icon (
1449- imageVector = Icons .Default .ContentCopy ,
1450- contentDescription = " Copy URL" ,
1451- modifier = Modifier .size(16 .dp),
1452- tint = Color .White .copy(alpha = 0.8f )
1453- )
1454- }
1455- }
1489+ DropdownMenuItem (
1490+ text = {
1491+ Row (
1492+ verticalAlignment = Alignment .CenterVertically ,
1493+ modifier = Modifier .fillMaxWidth(),
1494+ horizontalArrangement = Arrangement .SpaceBetween ,
1495+ ) {
1496+ Row (verticalAlignment = Alignment .CenterVertically ) {
1497+ Icon (
1498+ if (streamStatus.isServerRunning) Icons .Default .Wifi else Icons .Default .WifiOff ,
1499+ contentDescription = null ,
1500+ modifier = Modifier .size(18 .dp),
1501+ tint = MaterialTheme .colorScheme.onSurface,
1502+ )
1503+ Spacer (modifier = Modifier .width(12 .dp))
1504+ Text (
1505+ if (streamStatus.isServerRunning) " Turn off server" else " Turn on server" ,
1506+ style = MaterialTheme .typography.bodyMedium,
1507+ )
1508+ }
1509+ Switch (
1510+ checked = streamStatus.isServerRunning,
1511+ onCheckedChange = null ,
1512+ thumbContent = if (streamStatus.isServerRunning) {
1513+ { Icon (Icons .Default .Check , null , Modifier .size(12 .dp)) }
1514+ } else null ,
1515+ )
1516+ }
1517+ },
1518+ onClick = {
1519+ expanded = false
1520+ onToggleServer()
1521+ },
1522+ )
14561523 }
14571524 }
14581525}
0 commit comments