OXYGENHUB

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