fix(android): safe area + settings scroll

This commit is contained in:
Peter Steinberger
2025-12-14 04:34:32 +00:00
parent 3ef910d23e
commit ce915d3438
2 changed files with 138 additions and 115 deletions

View File

@@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@@ -34,17 +34,26 @@ import com.steipete.clawdis.node.MainViewModel
fun RootScreen(viewModel: MainViewModel) { fun RootScreen(viewModel: MainViewModel) {
var sheet by remember { mutableStateOf<Sheet?>(null) } var sheet by remember { mutableStateOf<Sheet?>(null) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val safeButtonInsets = WindowInsets.statusBars.only(WindowInsetsSides.Top) val safeButtonInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
CanvasView(viewModel = viewModel, modifier = Modifier.fillMaxSize().zIndex(0f)) CanvasView(viewModel = viewModel, modifier = Modifier.fillMaxSize().zIndex(0f))
Box(modifier = Modifier.align(Alignment.TopEnd).zIndex(1f).windowInsetsPadding(safeButtonInsets).padding(12.dp)) { Box(
Button(onClick = { sheet = Sheet.Settings }) { Text("Settings") } modifier =
} Modifier
.fillMaxSize()
.zIndex(1f)
.windowInsetsPadding(safeButtonInsets)
.padding(12.dp),
) {
Box(modifier = Modifier.align(Alignment.TopStart)) {
Button(onClick = { sheet = Sheet.Chat }) { Text("Chat") }
}
Box(modifier = Modifier.align(Alignment.TopStart).zIndex(1f).windowInsetsPadding(safeButtonInsets).padding(12.dp)) { Box(modifier = Modifier.align(Alignment.TopEnd)) {
Button(onClick = { sheet = Sheet.Chat }) { Text("Chat") } Button(onClick = { sheet = Sheet.Settings }) { Text("Settings") }
}
} }
} }

View File

@@ -4,17 +4,18 @@ import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
@@ -43,7 +44,7 @@ fun SettingsSheet(viewModel: MainViewModel) {
val serverName by viewModel.serverName.collectAsState() val serverName by viewModel.serverName.collectAsState()
val remoteAddress by viewModel.remoteAddress.collectAsState() val remoteAddress by viewModel.remoteAddress.collectAsState()
val bridges by viewModel.bridges.collectAsState() val bridges by viewModel.bridges.collectAsState()
val scrollState = rememberScrollState() val listState = rememberLazyListState()
val permissionLauncher = val permissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms -> rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { perms ->
@@ -51,125 +52,138 @@ fun SettingsSheet(viewModel: MainViewModel) {
viewModel.setCameraEnabled(cameraOk) viewModel.setCameraEnabled(cameraOk)
} }
Column( LazyColumn(
modifier = Modifier.fillMaxWidth().verticalScroll(scrollState).padding(16.dp), state = listState,
modifier = Modifier.fillMaxWidth().imePadding(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
) { ) {
Text("Node") item { Text("Node") }
OutlinedTextField( item {
value = displayName, OutlinedTextField(
onValueChange = viewModel::setDisplayName, value = displayName,
label = { Text("Name") }, onValueChange = viewModel::setDisplayName,
modifier = Modifier.fillMaxWidth(), label = { Text("Name") },
) modifier = Modifier.fillMaxWidth(),
Text("Instance ID: $instanceId")
HorizontalDivider()
Text("Camera")
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
Switch(
checked = cameraEnabled,
onCheckedChange = { enabled ->
if (!enabled) {
viewModel.setCameraEnabled(false)
return@Switch
}
val cameraOk =
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
PackageManager.PERMISSION_GRANTED
if (cameraOk) {
viewModel.setCameraEnabled(true)
} else {
permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
}
},
) )
Text(if (cameraEnabled) "Allow Camera" else "Camera Disabled")
} }
Text("Tip: grant Microphone permission for video clips with audio.") item { Text("Instance ID: $instanceId") }
HorizontalDivider() item { HorizontalDivider() }
Text("Bridge") item { Text("Camera") }
Text("Status: $statusText") item {
if (serverName != null) Text("Server: $serverName") Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
if (remoteAddress != null) Text("Address: $remoteAddress") Switch(
checked = cameraEnabled,
onCheckedChange = { enabled ->
if (!enabled) {
viewModel.setCameraEnabled(false)
return@Switch
}
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { val cameraOk =
Button( ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
onClick = { PackageManager.PERMISSION_GRANTED
viewModel.disconnect() if (cameraOk) {
NodeForegroundService.stop(context) viewModel.setCameraEnabled(true)
}, } else {
) { permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
Text("Disconnect") }
},
)
Text(if (cameraEnabled) "Allow Camera" else "Camera Disabled")
} }
} }
item { Text("Tip: grant Microphone permission for video clips with audio.") }
HorizontalDivider() item { HorizontalDivider() }
Text("Advanced") item { Text("Bridge") }
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) { item { Text("Status: $statusText") }
Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled) item { if (serverName != null) Text("Server: $serverName") }
Text(if (manualEnabled) "Manual Bridge Enabled" else "Manual Bridge Disabled") item { if (remoteAddress != null) Text("Address: $remoteAddress") }
} item {
OutlinedTextField( Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
value = manualHost, Button(
onValueChange = viewModel::setManualHost, onClick = {
label = { Text("Host") }, viewModel.disconnect()
modifier = Modifier.fillMaxWidth(), NodeForegroundService.stop(context)
enabled = manualEnabled, },
) ) {
OutlinedTextField( Text("Disconnect")
value = manualPort.toString(),
onValueChange = { v -> viewModel.setManualPort(v.toIntOrNull() ?: 0) },
label = { Text("Port") },
modifier = Modifier.fillMaxWidth(),
enabled = manualEnabled,
)
Button(
onClick = {
NodeForegroundService.start(context)
viewModel.connectManual()
},
enabled = manualEnabled,
) {
Text("Connect (Manual)")
}
HorizontalDivider()
Text("Discovered Bridges")
if (bridges.isEmpty()) {
Text("No bridges found yet.")
} else {
LazyColumn(modifier = Modifier.fillMaxWidth().height(240.dp)) {
items(bridges) { bridge ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
Text(bridge.name)
Text("${bridge.host}:${bridge.port}")
}
Spacer(modifier = Modifier.padding(4.dp))
Button(
onClick = {
NodeForegroundService.start(context)
viewModel.connect(bridge)
},
) {
Text("Connect")
}
}
HorizontalDivider()
} }
} }
} }
Spacer(modifier = Modifier.height(20.dp)) item { HorizontalDivider() }
item { Text("Advanced") }
item {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled)
Text(if (manualEnabled) "Manual Bridge Enabled" else "Manual Bridge Disabled")
}
}
item {
OutlinedTextField(
value = manualHost,
onValueChange = viewModel::setManualHost,
label = { Text("Host") },
modifier = Modifier.fillMaxWidth(),
enabled = manualEnabled,
)
}
item {
OutlinedTextField(
value = manualPort.toString(),
onValueChange = { v -> viewModel.setManualPort(v.toIntOrNull() ?: 0) },
label = { Text("Port") },
modifier = Modifier.fillMaxWidth(),
enabled = manualEnabled,
)
}
item {
Button(
onClick = {
NodeForegroundService.start(context)
viewModel.connectManual()
},
enabled = manualEnabled,
) {
Text("Connect (Manual)")
}
}
item { HorizontalDivider() }
item { Text("Discovered Bridges") }
if (bridges.isEmpty()) {
item { Text("No bridges found yet.") }
} else {
items(items = bridges, key = { it.stableId }) { bridge ->
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Column(modifier = Modifier.weight(1f)) {
Text(bridge.name)
Text("${bridge.host}:${bridge.port}")
}
Spacer(modifier = Modifier.padding(4.dp))
Button(
onClick = {
NodeForegroundService.start(context)
viewModel.connect(bridge)
},
) {
Text("Connect")
}
}
HorizontalDivider()
}
}
item { Spacer(modifier = Modifier.height(20.dp)) }
} }
} }