neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
Android CI wall: a header that fits a phone, and no black band
102b065 · gmorell · 2026-11-03 20:32
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