Android CI wall: a header that fits a phone, and no black band

102b065 · gmorell · 2026-11-03 20:32

12 files +46 -3
Message
{commit_body(@commit)}

Files changed

modified android/app/src/main/java/org/neiam/gitgud/app/ui/wall/CiWallScreen.kt
+46 −3
@@ -1,6 +1,8 @@
1 1 package org.neiam.gitgud.app.ui.wall
2 2
3 3 import android.app.Activity
4 +import android.os.Build
5 +import android.view.WindowManager
4 6 import android.view.KeyEvent as AndroidKeyEvent
5 7 import androidx.activity.compose.BackHandler
6 8 import androidx.compose.animation.core.LinearEasing
@@ -18,6 +20,9 @@ import androidx.compose.foundation.layout.Column
18 20 import androidx.compose.foundation.layout.PaddingValues
19 21 import androidx.compose.foundation.layout.Row
20 22 import androidx.compose.foundation.layout.Spacer
23 +import androidx.compose.foundation.layout.WindowInsets
24 +import androidx.compose.foundation.layout.safeDrawing
25 +import androidx.compose.foundation.layout.windowInsetsPadding
21 26 import androidx.compose.foundation.layout.fillMaxHeight
22 27 import androidx.compose.foundation.layout.fillMaxSize
23 28 import androidx.compose.foundation.layout.fillMaxWidth
@@ -34,6 +39,7 @@ import androidx.compose.material.icons.Icons
34 39 import androidx.compose.material.icons.automirrored.filled.ArrowBack
35 40 import androidx.compose.material.icons.filled.ArrowDropDown
36 41 import androidx.compose.material.icons.filled.Memory
42 +import androidx.compose.material.icons.filled.SwapVert
37 43 import androidx.compose.material3.DropdownMenu
38 44 import androidx.compose.material3.DropdownMenuItem
39 45 import androidx.compose.material3.FilterChip
@@ -199,6 +205,9 @@ fun CiWallScreen(
199 205 Column(
200 206 Modifier
201 207 .fillMaxSize()
208 + // Clear of the camera cutout, and of the system bars when
209 + // a swipe brings them back over the wall.
210 + .windowInsetsPadding(WindowInsets.safeDrawing)
202 211 .onPreviewKeyEvent { e ->
203 212 if (e.type == KeyEventType.KeyUp && e.nativeKeyEvent.keyCode == AndroidKeyEvent.KEYCODE_R) {
204 213 flip()
@@ -209,6 +218,7 @@ fun CiWallScreen(
209 218 },
210 219 ) {
211 220 WallHeader(
221 + compact = maxCols == null,
212 222 count = cards?.size,
213 223 columnCounts = maxCols?.let { WallTiling.columnCounts(cards?.size ?: 0, it) }.orEmpty(),
214 224 scope = scope,
@@ -274,6 +284,7 @@ private fun WallGrid(cards: List<WallCard>, maxCols: Int?, now: Instant) {
274 284
275 285 @Composable
276 286 private fun WallHeader(
287 + compact: Boolean,
277 288 count: Int?,
278 289 columnCounts: List<Int>,
279 290 scope: String?,
@@ -297,7 +308,7 @@ private fun WallHeader(
297 308 Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = theme.dim)
298 309 }
299 310 PulsingDot(if (error == null) theme.pending else theme.danger)
300 − Text("Live CI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
311 + Text("Live CI", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, maxLines = 1)
301 312
302 313 Box {
303 314 TextButton(onClick = { scopeMenu = true }) {
@@ -323,11 +334,12 @@ private fun WallHeader(
323 334 if (count == 1) "1 job" else "$count jobs",
324 335 color = theme.dim,
325 336 style = MaterialTheme.typography.labelMedium,
337 + maxLines = 1,
326 338 )
327 − ShapeGlyph(columnCounts, theme)
339 + if (!compact) ShapeGlyph(columnCounts, theme)
328 340 }
329 341
330 − if (error != null && count != null) {
342 + if (error != null && count != null && !compact) {
331 343 Text(
332 344 "offline · retrying",
333 345 color = theme.danger,
@@ -338,6 +350,18 @@ private fun WallHeader(
338 350
339 351 Spacer(Modifier.weight(1f))
340 352
353 + // A phone held upright has room for one control, not two chips.
354 + if (compact) {
355 + TextButton(
356 + onClick = { onNewestFirst(!newestFirst) },
357 + modifier = Modifier.focusRequester(orderFocus),
358 + ) {
359 + Icon(Icons.Default.SwapVert, contentDescription = null, tint = theme.accent)
360 + Text(if (newestFirst) "Newest" else "Oldest", color = theme.accent, maxLines = 1)
361 + }
362 + return@Row
363 + }
364 +
341 365 FilterChip(
342 366 selected = !newestFirst,
343 367 onClick = { onNewestFirst(false) },
@@ -632,12 +656,31 @@ private fun KeepAwakeAndImmersive() {
632 656 val window = (LocalContext.current as? Activity)?.window
633 657 DisposableEffect(view, window) {
634 658 view.keepScreenOn = true
659 + // Draw behind where the bars were and around the camera cutout,
660 + // rather than leaving a black band there; the wall pads itself
661 + // clear of both (safeDrawing).
662 + val cutoutMode = window?.let { w ->
663 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
664 + w.attributes.layoutInDisplayCutoutMode.also {
665 + w.attributes = w.attributes.apply {
666 + layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
667 + }
668 + }
669 + } else {
670 + null
671 + }
672 + }
673 + window?.let { WindowCompat.setDecorFitsSystemWindows(it, false) }
635 674 val bars = window?.let { WindowCompat.getInsetsController(it, view) }
636 675 bars?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
637 676 bars?.hide(WindowInsetsCompat.Type.systemBars())
638 677 onDispose {
639 678 view.keepScreenOn = false
640 679 bars?.show(WindowInsetsCompat.Type.systemBars())
680 + window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
681 + if (window != null && cutoutMode != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
682 + window.attributes = window.attributes.apply { layoutInDisplayCutoutMode = cutoutMode }
683 + }
641 684 }
642 685 }
643 686 }

Parents: 5ade69f