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