The quiet field behind empty and error screens, and screens that slide in from where they lead (refs #23) dce8ebd2482a
As the hub's error pages have the adrift scene, the app's empty and error states have it too. A screen opened comes in a little from the side it leads to and goes back the way it came. Backdrops draw nothing in previews and screenshot tests, which have no Rust core.
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 9d42c888b7d1
- When
- · 2026-10-28 01:24 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
dce8ebd2482a9b58500adc530ec3cff6215356ae15425c2efee92060203d8d18- Depends on
- 0a2858c0dad3 144cbf7bf2b5 1dfa631436b6 2a15621eca66 31e4f3755627 57c0824300db 62429abe5869 98dbd1220929 e833eb0a9e7f
- Depended on by
- d98e5a147428 6e2af63ae616
- In bookmarks
- main
- In tags
- v0.1.0 v0.1.1 v0.1.2 v0.1.3 v0.1.4 v0.1.5 v0.1.6 v0.1.7 v0.1.8 v0.1.9
- Edits
- 15 atoms, 0 tree operations, 4 files
Browse the tree at this change
modified CHANGELOG.md +1 −1
| … | 1 unchanged lines |
||
| 2 | 2 | ||
| 3 | 3 | ## Unreleased |
|
| 4 | 4 | ||
| 5 | - | - **The hub's living backgrounds (#23):** the website's backdrops come to the phone, the same Rust scenes (copied from OxygenHub's crates/backdrop by scripts/sync-backdrop.sh) stepped in rust-core and drawn by the app. Sign-in has the OxygenHub mark, which answers a finger; the dashboard has Your sky, your repositories and their last 30 days of pushes, proposals, issues, releases and runs, each mark a real event (OxygenHub's `GET /user/sky`; a hub without it shows the quiet field), drawing near your finger and naming the repository it rests by, behind cards it shows through. Live runs and jobs pulse. As on the web, drawing slows 3 s after the last touch and stops after 2 minutes, and it holds still with the system's animations off or battery saver on. |
|
| 5 | + | - **The hub's living backgrounds (#23):** the website's backdrops come to the phone, the same Rust scenes (copied from OxygenHub's crates/backdrop by scripts/sync-backdrop.sh) stepped in rust-core and drawn by the app. Sign-in has the OxygenHub mark, which answers a finger; the dashboard has Your sky, your repositories and their last 30 days of pushes, proposals, issues, releases and runs, each mark a real event (OxygenHub's `GET /user/sky`; a hub without it shows the quiet field), drawing near your finger and naming the repository it rests by, behind cards it shows through. Live runs and jobs pulse; empty and error screens have the quiet field the hub's error pages have; a screen opened slides in a little from the side it leads to, and goes back the way it came. As on the web, drawing slows 3 s after the last touch and stops after 2 minutes, and it holds still with the system's animations off or battery saver on. |
|
| 6 | 6 | - **Assign issues from the phone (#4):** an issue's Assignees action shows who it is assigned to, each removable, with Assign me and a field for anyone's username; saved as the whole set (triage access on the hub). |
|
| 7 | 7 | - **Approve, reject and expedite runs (#5):** a run's job that waits for a person shows Approve and Reject (its approval rules on the hub decide who may), and a live run has Expedite, which asks why it is urgent (a maintainer's, within the owner's cap). Needs OxygenHub 0.9.11 (the job's `approval` and the expedite route). |
|
| 8 | 8 | - **Review a proposal from the phone (#4):** an open proposal's Reviews section takes a review: Approve, Request changes or Comment, with what you think (needed to ask for changes or to comment), for a phone signed in to change things. An approval from the phone is not sealed, as it holds no key of yours: where a repository needs sealed approvals it does not count yet. |
|
| … | 22 unchanged lines |
modified app/src/main/java/io/oxygenhub/android/ui/backdrop/Backdrop.kt +8 −2
| … | 36 unchanged lines |
||
| 37 | 37 | import androidx.compose.ui.layout.onSizeChanged |
|
| 38 | 38 | import androidx.compose.ui.platform.LocalContext |
|
| 39 | 39 | import androidx.compose.ui.platform.LocalDensity |
|
| 40 | + | import androidx.compose.ui.platform.LocalInspectionMode |
|
| 40 | 41 | import androidx.compose.ui.unit.IntOffset |
|
| 41 | 42 | import androidx.compose.ui.unit.IntSize |
|
| 42 | 43 | import androidx.compose.ui.unit.dp |
|
| 43 | 44 | import io.oxygenhub.android.ui.theme.LocalStatusColors |
|
| 44 | - | import kotlinx.coroutines.isActive |
|
| 45 | - | import uniffi.rust_core.BackdropLabel |
|
| 46 | 45 | import java.nio.ByteBuffer |
|
| 47 | 46 | import java.nio.ByteOrder |
|
| 48 | 47 | import kotlin.math.roundToInt |
|
| 48 | + | import kotlinx.coroutines.isActive |
|
| 49 | 49 | import uniffi.rust_core.Backdrop as Scene |
|
| 50 | + | import uniffi.rust_core.BackdropLabel |
|
| 50 | 51 | ||
| 51 | 52 | /** |
|
| 52 | 53 | * The website's living backgrounds (#23, the hub's docs/backgrounds.md): |
|
| … | 12 unchanged lines |
||
| 65 | 66 | modifier: Modifier = Modifier, |
|
| 66 | 67 | content: @Composable BoxScope.() -> Unit, |
|
| 67 | 68 | ) { |
|
| 69 | + | // Previews and screenshot tests have no Rust core to step a scene with. |
|
| 70 | + | if (LocalInspectionMode.current) { |
|
| 71 | + | Box(modifier) { content() } |
|
| 72 | + | return |
|
| 73 | + | } |
|
| 68 | 74 | val context = LocalContext.current |
|
| 69 | 75 | val still = remember(context) { backdropStill(context) } |
|
| 70 | 76 | val scene = remember(key, still) { make(still) } |
|
| … | 180 unchanged lines |
modified app/src/main/java/io/oxygenhub/android/ui/common/States.kt +16 −11
| … | 23 unchanged lines |
||
| 24 | 24 | import androidx.compose.ui.graphics.vector.ImageVector |
|
| 25 | 25 | import androidx.compose.ui.text.style.TextAlign |
|
| 26 | 26 | import androidx.compose.ui.unit.dp |
|
| 27 | + | import io.oxygenhub.android.ui.backdrop.Backdrop |
|
| 28 | + | import uniffi.rust_core.Backdrop as Scene |
|
| 27 | 29 | ||
| 28 | 30 | /** A spinner in the middle of the space. */ |
|
| 29 | 31 | @Composable |
|
| … | 37 unchanged lines |
||
| 67 | 69 | modifier: Modifier = Modifier, |
|
| 68 | 70 | extra: @Composable () -> Unit = {}, |
|
| 69 | 71 | ) { |
|
| 70 | - | Column( |
|
| 71 | - | modifier.fillMaxSize().padding(32.dp), |
|
| 72 | - | horizontalAlignment = Alignment.CenterHorizontally, |
|
| 73 | - | verticalArrangement = Arrangement.Center, |
|
| 74 | - | ) { |
|
| 75 | - | Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) |
|
| 76 | - | Spacer(Modifier.height(16.dp)) |
|
| 77 | - | Text(title, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center) |
|
| 78 | - | Spacer(Modifier.height(8.dp)) |
|
| 79 | - | Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) |
|
| 80 | - | extra() |
|
| 72 | + | // The quiet field behind it, as behind the hub's error pages (#23). |
|
| 73 | + | Backdrop(key = Unit, make = { still -> Scene.adrift(still) }, modifier = modifier.fillMaxSize()) { |
|
| 74 | + | Column( |
|
| 75 | + | Modifier.fillMaxSize().padding(32.dp), |
|
| 76 | + | horizontalAlignment = Alignment.CenterHorizontally, |
|
| 77 | + | verticalArrangement = Arrangement.Center, |
|
| 78 | + | ) { |
|
| 79 | + | Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) |
|
| 80 | + | Spacer(Modifier.height(16.dp)) |
|
| 81 | + | Text(title, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center) |
|
| 82 | + | Spacer(Modifier.height(8.dp)) |
|
| 83 | + | Text(body, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center) |
|
| 84 | + | extra() |
|
| 85 | + | } |
|
| 81 | 86 | } |
|
| 82 | 87 | } |
|
| 83 | 88 | ||
| … | 24 unchanged lines |
modified app/src/main/java/io/oxygenhub/android/ui/nav/AppNavigation.kt +19 −3
| 1 | 1 | package io.oxygenhub.android.ui.nav |
|
| 2 | 2 | ||
| 3 | + | import androidx.compose.animation.AnimatedContentTransitionScope.SlideDirection |
|
| 4 | + | import androidx.compose.animation.core.tween |
|
| 5 | + | import androidx.compose.animation.fadeIn |
|
| 6 | + | import androidx.compose.animation.fadeOut |
|
| 3 | 7 | import androidx.compose.material.icons.Icons |
|
| 4 | 8 | import androidx.compose.material.icons.filled.AccountCircle |
|
| 5 | 9 | import androidx.compose.material.icons.filled.Book |
|
| … | 34 unchanged lines |
||
| 40 | 44 | import io.oxygenhub.android.auth.Session |
|
| 41 | 45 | import io.oxygenhub.android.ui.account.AccountScreen |
|
| 42 | 46 | import io.oxygenhub.android.ui.code.ChangeScreen |
|
| 43 | - | import io.oxygenhub.android.ui.owners.OwnerScreen |
|
| 44 | - | import io.oxygenhub.android.ui.owners.OwnerViewModel |
|
| 45 | 47 | import io.oxygenhub.android.ui.code.ChangeViewModel |
|
| 46 | 48 | import io.oxygenhub.android.ui.code.CodeViewModel |
|
| 47 | 49 | import io.oxygenhub.android.ui.code.FileScreen |
|
| … | 15 unchanged lines |
||
| 63 | 65 | import io.oxygenhub.android.ui.issues.IssuesViewModel |
|
| 64 | 66 | import io.oxygenhub.android.ui.issues.NewIssueScreen |
|
| 65 | 67 | import io.oxygenhub.android.ui.issues.NewIssueViewModel |
|
| 68 | + | import io.oxygenhub.android.ui.owners.OwnerScreen |
|
| 69 | + | import io.oxygenhub.android.ui.owners.OwnerViewModel |
|
| 66 | 70 | import io.oxygenhub.android.ui.projects.BoardScreen |
|
| 67 | 71 | import io.oxygenhub.android.ui.projects.BoardViewModel |
|
| 68 | 72 | import io.oxygenhub.android.ui.projects.ProjectsScreen |
|
| … | 117 unchanged lines |
||
| 186 | 190 | } |
|
| 187 | 191 | }, |
|
| 188 | 192 | ) { |
|
| 189 | - | NavHost(nav, startDestination = start.route) { |
|
| 193 | + | NavHost( |
|
| 194 | + | nav, |
|
| 195 | + | startDestination = start.route, |
|
| 196 | + | // A screen opened comes in a little from the side it leads to as it fades in, and |
|
| 197 | + | // goes back the way it came (#23): where you are going reads in the motion. |
|
| 198 | + | enterTransition = { slideIntoContainer(SlideDirection.Start, tween(NAV_MS), initialOffset = { it / 8 }) + fadeIn(tween(NAV_MS)) }, |
|
| 199 | + | exitTransition = { fadeOut(tween(NAV_MS * 2 / 3)) }, |
|
| 200 | + | popEnterTransition = { fadeIn(tween(NAV_MS)) }, |
|
| 201 | + | popExitTransition = { slideOutOfContainer(SlideDirection.End, tween(NAV_MS), targetOffset = { it / 8 }) + fadeOut(tween(NAV_MS * 2 / 3)) }, |
|
| 202 | + | ) { |
|
| 190 | 203 | composable<DashboardRoute> { |
|
| 191 | 204 | DashboardScreen( |
|
| 192 | 205 | vm = rememberViewModel { DashboardViewModel(api, session) }, |
|
| … | 154 unchanged lines |
||
| 347 | 360 | restoreState = true |
|
| 348 | 361 | } |
|
| 349 | 362 | } |
|
| 363 | + | ||
| 364 | + | /** How long a screen takes to come in (#23). */ |
|
| 365 | + | private const val NAV_MS = 300 |