v1.0.52: fix Select button cut off on Android 16 edge-to-edge dialogs

Android 15+ enforces edge-to-edge on Dialog windows, making standard
Compose WindowInsets APIs return 0 inside dialogs. Fix: use ViewCompat
insets listener inside the Dialog to read actual system bar heights,
with 56dp minimum to guarantee full nav bar clearance. Spacer inside
the button Surface lets the elevated background extend behind the nav
bar. Also make the entire local folder field tappable (not just the
trailing icon) for better UX.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 00:14:43 +00:00
parent 3c008ec8df
commit 99193af2c5
4 changed files with 76 additions and 31 deletions
@@ -1,6 +1,7 @@
package com.syncflow.ui.addpair package com.syncflow.ui.addpair
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@@ -105,18 +106,17 @@ fun AddPairScreen(onDone: () -> Unit, vm: AddPairViewModel = hiltViewModel()) {
Spacer(Modifier.height(4.dp)) Spacer(Modifier.height(4.dp))
// Local folder // Local folder
OutlinedTextField( Box(modifier = Modifier.fillMaxWidth()) {
value = uriToDisplay(s.localPath), onValueChange = {}, OutlinedTextField(
label = { Text("Local folder") }, value = uriToDisplay(s.localPath), onValueChange = {},
leadingIcon = { Icon(Icons.Default.PhoneAndroid, null) }, label = { Text("Local folder") },
trailingIcon = { leadingIcon = { Icon(Icons.Default.PhoneAndroid, null) },
IconButton(onClick = { showLocalBrowser = true }) { trailingIcon = { Icon(Icons.Default.FolderOpen, null) },
Icon(Icons.Default.FolderOpen, "Browse") readOnly = true, singleLine = true, modifier = Modifier.fillMaxWidth(),
} placeholder = { Text("Tap to choose folder…") },
}, )
readOnly = true, singleLine = true, modifier = Modifier.fillMaxWidth(), Box(modifier = Modifier.matchParentSize().clickable { showLocalBrowser = true })
placeholder = { Text("Tap to choose folder…") }, }
)
// Remote folder // Remote folder
OutlinedTextField( OutlinedTextField(
@@ -26,9 +26,14 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -131,8 +136,25 @@ fun LocalBrowserDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false), properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false),
) { ) {
val view = LocalView.current
val density = LocalDensity.current
var topInset by remember { mutableStateOf(0.dp) }
var bottomInset by remember { mutableStateOf(56.dp) }
DisposableEffect(view) {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
with(density) {
topInset = bars.top.toDp()
bottomInset = maxOf(bars.bottom.toDp(), 56.dp)
}
insets
}
ViewCompat.requestApplyInsets(view)
onDispose { ViewCompat.setOnApplyWindowInsetsListener(view, null) }
}
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {
Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) { Column(modifier = Modifier.fillMaxSize().padding(top = topInset)) {
// ── Top bar ────────────────────────────────────────────────── // ── Top bar ──────────────────────────────────────────────────
TopAppBar( TopAppBar(
@@ -242,20 +264,21 @@ fun LocalBrowserDialog(
// ── Select button ──────────────────────────────────────────── // ── Select button ────────────────────────────────────────────
Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) { Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.navigationBarsPadding()) { Column {
Button( Button(
onClick = { onSelect(currentPath.absolutePath) }, onClick = { onSelect(currentPath.absolutePath) },
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp) .padding(start = 16.dp, top = 12.dp, end = 16.dp, bottom = 12.dp)
.height(52.dp), .height(52.dp),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
) { ) {
Icon(Icons.Default.CheckCircle, null, Modifier.size(20.dp)) Icon(Icons.Default.CheckCircle, null, Modifier.size(20.dp))
Spacer(Modifier.width(8.dp)) Spacer(Modifier.width(8.dp))
Text("Select \"$currentFolderName\"", Text("Select \"$currentFolderName\"",
style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
} }
Spacer(Modifier.height(bottomInset))
} }
} }
} }
@@ -25,9 +25,13 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.syncflow.domain.model.RemoteFile import com.syncflow.domain.model.RemoteFile
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -77,8 +81,25 @@ fun RemoteBrowserDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false), properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false),
) { ) {
val view = LocalView.current
val density = LocalDensity.current
var topInset by remember { mutableStateOf(0.dp) }
var bottomInset by remember { mutableStateOf(56.dp) }
DisposableEffect(view) {
ViewCompat.setOnApplyWindowInsetsListener(view) { _, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
with(density) {
topInset = bars.top.toDp()
bottomInset = maxOf(bars.bottom.toDp(), 56.dp)
}
insets
}
ViewCompat.requestApplyInsets(view)
onDispose { ViewCompat.setOnApplyWindowInsetsListener(view, null) }
}
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.surface) {
Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) { Column(modifier = Modifier.fillMaxSize().padding(top = topInset)) {
// ── Top bar ────────────────────────────────────────────────── // ── Top bar ──────────────────────────────────────────────────
TopAppBar( TopAppBar(
@@ -208,7 +229,7 @@ fun RemoteBrowserDialog(
// ── Select button ──────────────────────────────────────────── // ── Select button ────────────────────────────────────────────
Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) { Surface(tonalElevation = 4.dp, modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.navigationBarsPadding()) { Column {
Button( Button(
onClick = { onSelect(state.currentPath) }, onClick = { onSelect(state.currentPath) },
modifier = Modifier modifier = Modifier
@@ -225,6 +246,7 @@ fun RemoteBrowserDialog(
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
) )
} }
Spacer(Modifier.height(bottomInset))
} }
} }
} }
+2 -2
View File
@@ -1,2 +1,2 @@
VERSION_NAME=1.0.47 VERSION_NAME=1.0.52
VERSION_CODE=48 VERSION_CODE=53