OXYGENHUB
# d1f635be9442 oxygenhub-android / app / src / screenshotTest / kotlin / io / oxygenhub / android / Screenshots.kt
9.0 KiB · last changed Proposals on the dashboard (closes #18) 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 userProposals() = UserProposals(
101
        review = listOf(ProposalBrief("babbage/difference-engine", 12, "Carry the tens with a ratchet", "babbage", updatedMs = now - 2 * hour)),
102
        authored = listOf(ProposalBrief("ada/engine", 44, "Note G: loop the Bernoulli numbers", "ada", updatedMs = now - 5 * hour)),
103
    )
104
105
    override suspend fun proposals(owner: String, name: String, state: ProposalState) = emptyList<Proposal>()
106
107
    override suspend fun proposal(owner: String, name: String, number: Long) = Proposal(number, "Note G")
108
109
    override suspend fun checks(owner: String, name: String, number: Long) = emptyList<Check>()
110
111
    override suspend fun land(owner: String, name: String, number: Long) = proposal(owner, name, number)
112
113
    override suspend fun setAutoLand(owner: String, name: String, number: Long, on: Boolean) = proposal(owner, name, number)
114
115
    // Ada's identicon, as the hub would draw it.
116
    override suspend fun avatar(name: String) =
117
        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))
118
}
119
120
private val session = Session("https://oxygenhub.io", "ada", "", listOf("repo:read", "user:read"))
121
122
@Composable
123
private fun App(start: TopLevel, selected: String? = null, dark: Boolean = false) {
124
    // layoutlib never runs the main looper, so the view models' first load
125
    // would never land: run it in place (the sample hub never suspends).
126
    remember { Dispatchers.setMain(Dispatchers.Unconfined) }
127
    OxygenHubTheme(darkTheme = dark, dynamicColor = false) {
128
        SignedInApp(session, SampleHub(), { "Hello, ada! Oxygen Native Core is active." }, {}, start, selected)
129
    }
130
}
131
132
@PreviewTest
133
@Preview(name = "sign-in", device = PHONE, showSystemUi = false)
134
@Composable
135
fun SignInPhone() {
136
    OxygenHubTheme(dynamicColor = false) {
137
        SignInContent("https://oxygenhub.io", SignInState.Idle(), null, {}, {}, {})
138
    }
139
}
140
141
@PreviewTest
142
@Preview(name = "sign-in-code", device = PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES)
143
@Composable
144
fun SignInCodeDark() {
145
    val code = DeviceCode("", "BCDF-GHJK", "https://oxygenhub.io/device", "https://oxygenhub.io/device?code=BCDF-GHJK")
146
    OxygenHubTheme(darkTheme = true, dynamicColor = false) {
147
        SignInContent("https://oxygenhub.io", SignInState.Approving(code), null, {}, {}, {})
148
    }
149
}
150
151
@PreviewTest
152
@Preview(name = "dashboard-phone", device = PHONE)
153
@Composable
154
fun DashboardPhone() = App(TopLevel.Dashboard)
155
156
@PreviewTest
157
@Preview(name = "repositories-foldable", device = FOLDABLE)
158
@Composable
159
fun RepositoriesFoldable() = App(TopLevel.Repositories, selected = "ada/engine")
160
161
@PreviewTest
162
@Preview(name = "repositories-tablet-dark", device = TABLET, uiMode = Configuration.UI_MODE_NIGHT_YES)
163
@Composable
164
fun RepositoriesTabletDark() = App(TopLevel.Repositories, selected = "ada/engine", dark = true)
165
166
@PreviewTest
167
@Preview(name = "inbox-phone-dark", device = PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES)
168
@Composable
169
fun InboxPhoneDark() = App(TopLevel.Inbox, dark = true)
170
171
/**
172
 * The launcher icon as launchers mask it (a circle, a squircle) and as a
173
 * themed icon (its monochrome layer, tinted), on the README's page.
174
 */
175
@PreviewTest
176
@Preview(name = "launcher-icon", widthDp = 400, heightDp = 132, backgroundColor = 0xFFF4F7FC, showBackground = true)
177
@Composable
178
fun LauncherIcon() {
179
    Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
180
        for (shape in listOf(CircleShape, RoundedCornerShape(30))) {
181
            Box(Modifier.size(108.dp).clip(shape)) {
182
                Image(painterResource(R.drawable.ic_launcher_background), null, Modifier.fillMaxSize())
183
                Image(painterResource(R.drawable.ic_launcher_foreground), null, Modifier.fillMaxSize())
184
            }
185
        }
186
        Box(Modifier.size(108.dp).clip(CircleShape).background(Color(0xFFD3E3FD))) {
187
            Image(
188
                painterResource(R.drawable.ic_launcher_monochrome),
189
                null,
190
                Modifier.fillMaxSize(),
191
                colorFilter = ColorFilter.tint(Color(0xFF041E49)),
192
            )
193
        }
194
    }
195
}