package io.oxygenhub.android import android.content.res.Configuration import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.tooling.preview.Preview import com.android.tools.screenshot.PreviewTest import io.oxygenhub.android.api.Avatar import io.oxygenhub.android.api.DeviceCode import io.oxygenhub.android.api.HubApi import io.oxygenhub.android.api.Inbox import io.oxygenhub.android.api.Issue import io.oxygenhub.android.api.IssueState import io.oxygenhub.android.api.Label import io.oxygenhub.android.api.Notification import io.oxygenhub.android.api.Repository import io.oxygenhub.android.api.User import io.oxygenhub.android.api.Visibility import io.oxygenhub.android.auth.Session import io.oxygenhub.android.ui.nav.SignedInApp import io.oxygenhub.android.ui.nav.TopLevel import io.oxygenhub.android.ui.signin.SignInContent import io.oxygenhub.android.ui.signin.SignInState import io.oxygenhub.android.ui.theme.OxygenHubTheme import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.setMain // The README's screenshots, rendered by layoutlib with no emulator: // ./gradlew :app:updateDebugScreenshotTest // writes them under app/src/screenshotTestDebug/reference; scripts/screenshots.sh // copies them to docs/screenshots. The data is made up. private const val PHONE = "spec:width=411dp,height=891dp" private const val FOLDABLE = "spec:width=791dp,height=820dp" private const val TABLET = "spec:width=1280dp,height=800dp,dpi=240" private val hour = 3_600_000L private val day = 24 * hour /** A hub in memory, for the pictures. */ private class SampleHub : HubApi { private val now = System.currentTimeMillis() override suspend fun user() = User("ada", "Ada Lovelace") override suspend fun repositories() = listOf( Repository("ada", "engine", "The analytical engine's notes, in Oxygen", visibility = Visibility.Public, updatedMs = now - 2 * hour), Repository("ada", "bernoulli", "Computing Bernoulli numbers, note G", visibility = Visibility.Private, updatedMs = now - 1 * day), Repository("ada", "punch-cards", "Card decks for the Jacquard loom", updatedMs = now - 3 * day), Repository("babbage", "difference-engine", "Engine No. 2", visibility = Visibility.Internal, updatedMs = now - 5 * hour), Repository("ada", "letters", "Correspondence, sealed and signed", updatedMs = now - 12 * day), ) override suspend fun issues(owner: String, name: String, state: IssueState) = listOf( Issue( 42, "Note G: the loop that computes B7", author = "ada", pinned = true, comments = 4, labels = listOf(Label("feature", "#0064e0")), createdMs = now - 3 * hour, ), Issue(41, "The mill overflows on forty-digit carries", author = "babbage", comments = 2, labels = listOf(Label("bug", "#c62d47")), createdMs = now - 1 * day), Issue(39, "Describe the store's capacity in the README", author = "ada", labels = listOf(Label("docs", "#00b8d4")), createdMs = now - 4 * day), Issue(37, "Punch-card reader skips every hundredth card", author = "menabrea", comments = 7, createdMs = now - 9 * day), ).filter { state == IssueState.All || it.state == state.query } override suspend fun inbox(limit: Int) = Inbox( unread = 3, items = listOf( Notification(5, now - 20 * 60_000L, "proposal.review_requested", "Carry look-ahead for the mill", actor = "babbage"), Notification(4, now - 2 * hour, "flow.failed", "engine main: tests failed"), Notification(3, now - 5 * hour, "issue.comment", "Note G: the loop that computes B7", actor = "menabrea"), Notification(2, now - 1 * day, "proposal.landed", "Store of a thousand numbers", actor = "ada", read = true), Notification(1, now - 2 * day, "issue.assigned", "The mill overflows on forty-digit carries", actor = "babbage", read = true), ), ) override suspend fun markRead(id: Long) {} override suspend fun markAllRead() {} override suspend fun revokeToken() {} // Ada's identicon, as the hub would draw it. override suspend fun avatar(name: String) = 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)) } private val session = Session("https://oxygenhub.io", "ada", "", listOf("repo:read", "user:read")) @Composable private fun App(start: TopLevel, selected: String? = null, dark: Boolean = false) { // layoutlib never runs the main looper, so the view models' first load // would never land: run it in place (the sample hub never suspends). remember { Dispatchers.setMain(Dispatchers.Unconfined) } OxygenHubTheme(darkTheme = dark, dynamicColor = false) { SignedInApp(session, SampleHub(), { "Hello, ada! Oxygen Native Core is active." }, {}, start, selected) } } @PreviewTest @Preview(name = "sign-in", device = PHONE, showSystemUi = false) @Composable fun SignInPhone() { OxygenHubTheme(dynamicColor = false) { SignInContent("https://oxygenhub.io", SignInState.Idle(), null, {}, {}, {}) } } @PreviewTest @Preview(name = "sign-in-code", device = PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun SignInCodeDark() { val code = DeviceCode("", "BCDF-GHJK", "https://oxygenhub.io/device", "https://oxygenhub.io/device?code=BCDF-GHJK") OxygenHubTheme(darkTheme = true, dynamicColor = false) { SignInContent("https://oxygenhub.io", SignInState.Approving(code), null, {}, {}, {}) } } @PreviewTest @Preview(name = "dashboard-phone", device = PHONE) @Composable fun DashboardPhone() = App(TopLevel.Dashboard) @PreviewTest @Preview(name = "repositories-foldable", device = FOLDABLE) @Composable fun RepositoriesFoldable() = App(TopLevel.Repositories, selected = "ada/engine") @PreviewTest @Preview(name = "repositories-tablet-dark", device = TABLET, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun RepositoriesTabletDark() = App(TopLevel.Repositories, selected = "ada/engine", dark = true) @PreviewTest @Preview(name = "inbox-phone-dark", device = PHONE, uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable fun InboxPhoneDark() = App(TopLevel.Inbox, dark = true) /** * The launcher icon as launchers mask it (a circle, a squircle) and as a * themed icon (its monochrome layer, tinted), on the README's page. */ @PreviewTest @Preview(name = "launcher-icon", widthDp = 400, heightDp = 132, backgroundColor = 0xFFF4F7FC, showBackground = true) @Composable fun LauncherIcon() { Row(Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(16.dp)) { for (shape in listOf(CircleShape, RoundedCornerShape(30))) { Box(Modifier.size(108.dp).clip(shape)) { Image(painterResource(R.drawable.ic_launcher_background), null, Modifier.fillMaxSize()) Image(painterResource(R.drawable.ic_launcher_foreground), null, Modifier.fillMaxSize()) } } Box(Modifier.size(108.dp).clip(CircleShape).background(Color(0xFFD3E3FD))) { Image( painterResource(R.drawable.ic_launcher_monochrome), null, Modifier.fillMaxSize(), colorFilter = ColorFilter.tint(Color(0xFF041E49)), ) } } }