OXYGENHUB
# 1c42a70fcba3 oxygenhub-android / app / src / screenshotTest / kotlin / io / oxygenhub / android / Screenshots.kt
9.1 KiB · last changed The phone as an OxygenFlow runner, first part (refs #8) 11 days ago source blame history raw
1
package io.oxygenhub.android
2
3
import android.content.res.Configuration
4
import androidx.compose.foundation.Image
5
import androidx.compose.foundation.background
6
import androidx.compose.foundation.layout.Arrangement
7
import androidx.compose.foundation.layout.Box
8
import androidx.compose.foundation.layout.Row
9
import androidx.compose.foundation.layout.fillMaxSize
10
import androidx.compose.foundation.layout.padding
11
import androidx.compose.foundation.layout.size
12
import androidx.compose.foundation.shape.CircleShape
13
import androidx.compose.foundation.shape.RoundedCornerShape
14
import androidx.compose.ui.Modifier
15
import androidx.compose.ui.draw.clip
16
import androidx.compose.ui.graphics.Color
17
import androidx.compose.ui.graphics.ColorFilter
18
import androidx.compose.ui.res.painterResource
19
import androidx.compose.ui.unit.dp
20
import androidx.compose.runtime.Composable
21
import androidx.compose.runtime.remember
22
import androidx.compose.ui.tooling.preview.Preview
23
import com.android.tools.screenshot.PreviewTest
24
import io.oxygenhub.android.api.Avatar
25
import io.oxygenhub.android.api.Check
26
import io.oxygenhub.android.api.Proposal
27
import io.oxygenhub.android.api.ProposalBrief
28
import io.oxygenhub.android.api.ProposalState
29
import io.oxygenhub.android.api.UserProposals
30
import io.oxygenhub.android.api.DeviceCode
31
import io.oxygenhub.android.api.HubApi
32
import io.oxygenhub.android.api.Inbox
33
import io.oxygenhub.android.api.Issue
34
import io.oxygenhub.android.api.IssueState
35
import io.oxygenhub.android.api.Label
36
import io.oxygenhub.android.api.Notification
37
import io.oxygenhub.android.api.Repository
38
import io.oxygenhub.android.api.User
39
import io.oxygenhub.android.api.Visibility
40
import io.oxygenhub.android.auth.Session
41
import io.oxygenhub.android.ui.nav.SignedInApp
42
import io.oxygenhub.android.ui.nav.TopLevel
43
import io.oxygenhub.android.ui.signin.SignInContent
44
import io.oxygenhub.android.ui.signin.SignInState
45
import io.oxygenhub.android.ui.theme.OxygenHubTheme
46
import kotlinx.coroutines.Dispatchers
47
import kotlinx.coroutines.test.setMain
48
49
// The README's screenshots, rendered by layoutlib with no emulator:
50
//   ./gradlew :app:updateDebugScreenshotTest
51
// writes them under app/src/screenshotTestDebug/reference; scripts/screenshots.sh
52
// copies them to docs/screenshots. The data is made up.
53
54
private const val PHONE = "spec:width=411dp,height=891dp"
55
private const val FOLDABLE = "spec:width=791dp,height=820dp"
56
private const val TABLET = "spec:width=1280dp,height=800dp,dpi=240"
57
58
private val hour = 3_600_000L
59
private val day = 24 * hour
60
61
/** A hub in memory, for the pictures. */
62
private class SampleHub : HubApi {
63
    private val now = System.currentTimeMillis()
64
65
    override suspend fun user() = User("ada", "Ada Lovelace")
66
67
    override suspend fun repositories() = listOf(
68
        Repository("ada", "engine", "The analytical engine's notes, in Oxygen", visibility = Visibility.Public, updatedMs = now - 2 * hour),
69
        Repository("ada", "bernoulli", "Computing Bernoulli numbers, note G", visibility = Visibility.Private, updatedMs = now - 1 * day),
70
        Repository("ada", "punch-cards", "Card decks for the Jacquard loom", updatedMs = now - 3 * day),
71
        Repository("babbage", "difference-engine", "Engine No. 2", visibility = Visibility.Internal, updatedMs = now - 5 * hour),
72
        Repository("ada", "letters", "Correspondence, sealed and signed", updatedMs = now - 12 * day),
73
    )
74
75
    override suspend fun issues(owner: String, name: String, state: IssueState) = listOf(
76
        Issue(
77
            42, "Note G: the loop that computes B7", author = "ada", pinned = true, comments = 4,
78
            labels = listOf(Label("feature", "#0064e0")), createdMs = now - 3 * hour,
79
        ),
80
        Issue(41, "The mill overflows on forty-digit carries", author = "babbage", comments = 2, labels = listOf(Label("bug", "#c62d47")), createdMs = now - 1 * day),
81
        Issue(39, "Describe the store's capacity in the README", author = "ada", labels = listOf(Label("docs", "#00b8d4")), createdMs = now - 4 * day),
82
        Issue(37, "Punch-card reader skips every hundredth card", author = "menabrea", comments = 7, createdMs = now - 9 * day),
83
    ).filter { state == IssueState.All || it.state == state.query }
84
85
    override suspend fun inbox(limit: Int) = Inbox(
86
        unread = 3,
87
        items = listOf(
88
            Notification(5, now - 20 * 60_000L, "proposal.review_requested", "Carry look-ahead for the mill", actor = "babbage"),
89
            Notification(4, now - 2 * hour, "flow.failed", "engine main: tests failed"),
90
            Notification(3, now - 5 * hour, "issue.comment", "Note G: the loop that computes B7", actor = "menabrea"),
91
            Notification(2, now - 1 * day, "proposal.landed", "Store of a thousand numbers", actor = "ada", read = true),
92
            Notification(1, now - 2 * day, "issue.assigned", "The mill overflows on forty-digit carries", actor = "babbage", read = true),
93
        ),
94
    )
95
96
    override suspend fun markRead(id: Long) {}
97
    override suspend fun markAllRead() {}
98
    override suspend fun revokeToken() {}
99
100
    override suspend fun runnerToken() = io.oxygenhub.android.api.RunnerToken("flowreg_sample", "ada")
101
102
    override suspend fun userProposals() = UserProposals(
103
        review = listOf(ProposalBrief("babbage/difference-engine", 12, "Carry the tens with a ratchet", "babbage", updatedMs = now - 2 * hour)),
104
        authored = listOf(ProposalBrief("ada/engine", 44, "Note G: loop the Bernoulli numbers", "ada", updatedMs = now - 5 * hour)),
105
    )
106
107
    override suspend fun proposals(owner: String, name: String, state: ProposalState) = emptyList<Proposal>()
108
109
    override suspend fun proposal(owner: String, name: String, number: Long) = Proposal(number, "Note G")
110
111
    override suspend fun checks(owner: String, name: String, number: Long) = emptyList<Check>()
112
113
    override suspend fun land(owner: String, name: String, number: Long) = proposal(owner, name, number)
114
115
    override suspend fun setAutoLand(owner: String, name: String, number: Long, on: Boolean) = proposal(owner, name, number)
116
117
    // Ada's identicon, as the hub would draw it.
118
    override suspend fun avatar(name: String) =
119
        Avatar.Identicon(196, setOf(1 to 1, 5 to 1, 2 to 2, 3 to 2, 4 to 2, 1 to 3, 3 to 3, 5 to 3, 2 to 4, 4 to 4, 1 to 5, 5 to 5))
120
}
121
122
private val session = Session("https://oxygenhub.io", "ada", "", listOf("repo:read", "user:read"))
123
124
@Composable
125
private fun App(start: TopLevel, selected: String? = null, dark: Boolean = false) {
126
    // layoutlib never runs the main looper, so the view models' first load
127
    // would never land: run it in place (the sample hub never suspends).
128
    remember { Dispatchers.setMain(Dispatchers.Unconfined) }
129
    OxygenHubTheme(darkTheme = dark, dynamicColor = false) {
130
        SignedInApp(session, SampleHub(), { "Hello, ada! Oxygen Native Core is active." }, {}, start, selected)
131
    }
132
}
133
134
@PreviewTest
135
@Preview(name = "sign-in", device = PHONE, showSystemUi = false)
136
@Composable
137
fun SignInPhone() {
138
    OxygenHubTheme(dynamicColor = false) {
139
        SignInContent("https://oxygenhub.io", SignInState.Idle(), null, {}, {}, {})
140
    }
141
}
142
143
@PreviewTest
144
@Preview(name = "sign-in-code", device = PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES)
145
@Composable
146
fun SignInCodeDark() {
147
    val code = DeviceCode("", "BCDF-GHJK", "https://oxygenhub.io/device", "https://oxygenhub.io/device?code=BCDF-GHJK")
148
    OxygenHubTheme(darkTheme = true, dynamicColor = false) {
149
        SignInContent("https://oxygenhub.io", SignInState.Approving(code), null, {}, {}, {})
150
    }
151
}
152
153
@PreviewTest
154
@Preview(name = "dashboard-phone", device = PHONE)
155
@Composable
156
fun DashboardPhone() = App(TopLevel.Dashboard)
157
158
@PreviewTest
159
@Preview(name = "repositories-foldable", device = FOLDABLE)
160
@Composable
161
fun RepositoriesFoldable() = App(TopLevel.Repositories, selected = "ada/engine")
162
163
@PreviewTest
164
@Preview(name = "repositories-tablet-dark", device = TABLET, uiMode = Configuration.UI_MODE_NIGHT_YES)
165
@Composable
166
fun RepositoriesTabletDark() = App(TopLevel.Repositories, selected = "ada/engine", dark = true)
167
168
@PreviewTest
169
@Preview(name = "inbox-phone-dark", device = PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES)
170
@Composable
171
fun InboxPhoneDark() = App(TopLevel.Inbox, dark = true)
172
173
/**
174
 * The launcher icon as launchers mask it (a circle, a squircle) and as a
175
 * themed icon (its monochrome layer, tinted), on the README's page.
176
 */
177
@PreviewTest
178
@Preview(name = "launcher-icon", widthDp = 400, heightDp = 132, backgroundColor = 0xFFF4F7FC, showBackground = true)
179
@Composable
180
fun LauncherIcon() {
181
    Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
182
        for (shape in listOf(CircleShape, RoundedCornerShape(30))) {
183
            Box(Modifier.size(108.dp).clip(shape)) {
184
                Image(painterResource(R.drawable.ic_launcher_background), null, Modifier.fillMaxSize())
185
                Image(painterResource(R.drawable.ic_launcher_foreground), null, Modifier.fillMaxSize())
186
            }
187
        }
188
        Box(Modifier.size(108.dp).clip(CircleShape).background(Color(0xFFD3E3FD))) {
189
            Image(
190
                painterResource(R.drawable.ic_launcher_monochrome),
191
                null,
192
                Modifier.fillMaxSize(),
193
                colorFilter = ColorFilter.tint(Color(0xFF041E49)),
194
            )
195
        }
196
    }
197
}