fix(android): safe area + settings scroll
This commit is contained in:
@@ -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") }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user