OXYGENHUB

Panels you can see: the hub's palette by default, clear surface steps and a 1 dp edge on every panel (closes #24) 840c75813f86

Cards were near-black on black on a phone: the app always used the wallpaper's dynamic colours, whose dark container tones sit a shade apart, and panels had no edge. Now OxygenHub's palette by default (Account › Wallpaper colours turns Android's on), with dark containers a clear step apart and brighter edges in both; every card is a Panel (an outlined card on surfaceContainerLow whose contents show its tone); wallpaper colours in the dark are lifted the same way. Backdrops clip to their bounds, and Markdown images the app cannot show take no room instead of a 180 dp placeholder.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 4b52d4cb73d0
When
· 2026-10-28 16:12 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
840c75813f8616e0bb4ad687ad0cfd9e57feef640579e2ce706f9ddd838399ad
Depends on
144cbf7bf2b5 1dfa631436b6 206d05a042e7 23279b95b5b2 2a15621eca66 31e4f3755627 40ab4df121e0 4aee5293461d 57c0824300db 98dbd1220929 af9f2e514c72 d1f635be9442 db00632a61d6 dfbc3e972872 e0004d61958c e2df27a81e5d e833eb0a9e7f ff65ea456ff1
Depended on by
263c6cad5e51 c53e328b2db4 1823bc12eda1 821035db5a1c d98e5a147428
In bookmarks
main
In tags
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
69 atoms, 4 tree operations, 20 files

Browse the tree at this change

added app/src/main/java/io/oxygenhub/android/ui/common/MarkdownImages.kt +20 −0
1 +
package io.oxygenhub.android.ui.common
2 +
3 +
import androidx.compose.runtime.Composable
4 +
import androidx.compose.ui.geometry.Size
5 +
import androidx.compose.ui.unit.Density
6 +
import com.mikepenz.markdown.model.ImageData
7 +
import com.mikepenz.markdown.model.ImageTransformer
8 +
import com.mikepenz.markdown.model.PlaceholderConfig
9 +
10 +
/**
11 +
 * Images in Markdown (#24): the app fetches none yet, so each takes no room,
12 +
 * where the renderer's default left a 180 dp blank (a README's badge, say).
13 +
 */
14 +
object NoImages : ImageTransformer {
15 +
    @Composable
16 +
    override fun transform(link: String): ImageData? = null
17 +
18 +
    override fun placeholderConfig(density: Density, containerSize: Size, intrinsicImageSize: Size): PlaceholderConfig =
19 +
        PlaceholderConfig(Size.Zero)
20 +
}
added app/src/main/java/io/oxygenhub/android/ui/common/Panel.kt +35 −0
1 +
package io.oxygenhub.android.ui.common
2 +
3 +
import androidx.compose.foundation.BorderStroke
4 +
import androidx.compose.foundation.layout.ColumnScope
5 +
import androidx.compose.material3.CardColors
6 +
import androidx.compose.material3.CardDefaults
7 +
import androidx.compose.material3.MaterialTheme
8 +
import androidx.compose.material3.OutlinedCard
9 +
import androidx.compose.runtime.Composable
10 +
import androidx.compose.ui.Modifier
11 +
import androidx.compose.ui.graphics.Color
12 +
import androidx.compose.ui.unit.dp
13 +
14 +
/**
15 +
 * A panel you can see on any screen (#24): Material's outlined card on a
16 +
 * tone a clear step above the background, with a 1 dp edge, as the hub's
17 +
 * pages give theirs. Every card in the app is one.
18 +
 */
19 +
@Composable
20 +
fun Panel(modifier: Modifier = Modifier, colors: CardColors = panelColors(), content: @Composable ColumnScope.() -> Unit) {
21 +
    OutlinedCard(modifier, colors = colors, border = panelBorder()) {
22 +
        // What sits on a panel shows the panel: a ListItem, say, paints
23 +
        // `surface`, which is the screen's own tone.
24 +
        val scope = this
25 +
        MaterialTheme(colorScheme = MaterialTheme.colorScheme.copy(surface = Color.Transparent)) { scope.content() }
26 +
    }
27 +
}
28 +
29 +
/** A panel's colours; `alpha` below 1 lets a backdrop show through. */
30 +
@Composable
31 +
fun panelColors(alpha: Float = 1f): CardColors =
32 +
    CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = alpha))
33 +
34 +
@Composable
35 +
fun panelBorder(): BorderStroke = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant)
added app/src/main/java/io/oxygenhub/android/ui/theme/Appearance.kt +37 −0
1 +
package io.oxygenhub.android.ui.theme
2 +
3 +
import android.content.Context
4 +
import androidx.compose.runtime.Composable
5 +
import androidx.compose.runtime.collectAsState
6 +
import androidx.compose.runtime.getValue
7 +
import androidx.compose.ui.platform.LocalContext
8 +
import kotlinx.coroutines.flow.MutableStateFlow
9 +
import kotlinx.coroutines.flow.StateFlow
10 +
import kotlinx.coroutines.flow.asStateFlow
11 +
12 +
/**
13 +
 * Whether the app takes its colours from the wallpaper (#24). Off by
14 +
 * default: the hub's own palette, as oxygenhub.io and its backdrops have.
15 +
 */
16 +
object Appearance {
17 +
    private const val PREFS = "appearance"
18 +
    private const val WALLPAPER = "wallpaper_colours"
19 +
    private var state: MutableStateFlow<Boolean>? = null
20 +
21 +
    fun wallpaperColours(context: Context): StateFlow<Boolean> =
22 +
        (state ?: MutableStateFlow(prefs(context).getBoolean(WALLPAPER, false)).also { state = it }).asStateFlow()
23 +
24 +
    fun setWallpaperColours(context: Context, on: Boolean) {
25 +
        prefs(context).edit().putBoolean(WALLPAPER, on).apply()
26 +
        wallpaperColours(context)
27 +
        state?.value = on
28 +
    }
29 +
30 +
    private fun prefs(context: Context) = context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
31 +
}
32 +
33 +
@Composable
34 +
fun rememberWallpaperColours(): Boolean {
35 +
    val on by Appearance.wallpaperColours(LocalContext.current).collectAsState()
36 +
    return on
37 +
}
added app/src/test/java/io/oxygenhub/android/ui/ThemeTest.kt +30 −0
1 +
package io.oxygenhub.android.ui
2 +
3 +
import androidx.compose.material3.darkColorScheme
4 +
import androidx.compose.ui.graphics.Color
5 +
import androidx.compose.ui.graphics.luminance
6 +
import io.oxygenhub.android.ui.theme.lifted
7 +
import org.junit.Assert.assertTrue
8 +
import org.junit.Test
9 +
10 +
class ThemeTest {
11 +
    @Test
12 +
    fun wallpaperColoursInTheDarkLiftEachContainerAStepAboveTheLast() {
13 +
        // Android's dark tones a shade apart, as a near-black wallpaper gives them.
14 +
        val near = Color(0xFF121212)
15 +
        val s = lifted(
16 +
            darkColorScheme(
17 +
                background = Color(0xFF0F0F0F),
18 +
                onSurface = Color(0xFFE6E6E6),
19 +
                surfaceContainerLow = near,
20 +
                surfaceContainer = near,
21 +
                surfaceContainerHigh = near,
22 +
                surfaceContainerHighest = near,
23 +
            ),
24 +
        )
25 +
        val steps = listOf(s.background, s.surfaceContainerLow, s.surfaceContainer, s.surfaceContainerHigh, s.surfaceContainerHighest, s.outlineVariant)
26 +
            .map { it.luminance() }
27 +
        assertTrue("each above the last: $steps", steps.zipWithNext().all { (a, b) -> b > a })
28 +
        assertTrue("a panel's edge stands well off the background: $steps", steps.last() > steps.first() * 3)
29 +
    }
30 +
}
modified CHANGELOG.md +4 −0
1 1
# Changelog
2 2
3 +
## Unreleased
4 +
5 +
- **Panels you can see (#24):** on a phone, cards and boxes were near-black on black. The app now uses OxygenHub's own colours by default (Account › Wallpaper colours turns Android's back on), every card is one panel with a 1 dp edge, as the website's have, on a tone a clear step above the background, and wallpaper colours in the dark get the same steps. A backdrop stays inside its band, and an image the app cannot show yet (a README's badge) no longer leaves a 180 dp blank.
6 +
3 7
## 0.1.0 (2026-10-28)
4 8
5 9
- **The release's secrets checked in seconds (#7):** `oh run start --job release-check` runs `scripts/release-check.sh` with no build: the keystore's SHA-256 and the entry its password opens (to compare with the keystore you hold), that the key `oxygenhub` opens with that password as signing needs, and that Firebase's settings are whole JSON; never a key, a password or the settings. The release job runs the same check first.
…
24 unchanged lines
modified app/src/main/java/io/oxygenhub/android/alerts/LandAlertsCard.kt +2 −2
…
9 unchanged lines
10 10
import androidx.compose.foundation.layout.Row
11 11
import androidx.compose.foundation.layout.fillMaxWidth
12 12
import androidx.compose.foundation.layout.padding
13 -
import androidx.compose.material3.ElevatedCard
14 13
import androidx.compose.material3.MaterialTheme
15 14
import androidx.compose.material3.Switch
16 15
import androidx.compose.material3.Text
…
6 unchanged lines
23 22
import androidx.compose.ui.unit.dp
24 23
import androidx.core.content.ContextCompat
25 24
import androidx.lifecycle.compose.collectAsStateWithLifecycle
25 +
import io.oxygenhub.android.ui.common.Panel
26 26
27 27
/** On the Account screen: notifications when a proposal can be landed (#20), on or off. */
28 28
@Composable
…
10 unchanged lines
39 39
            ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
40 40
        if (granted) LandAlerts.enable(context) else ask.launch(Manifest.permission.POST_NOTIFICATIONS)
41 41
    }
42 -
    ElevatedCard(Modifier.fillMaxWidth()) {
42 +
    Panel(Modifier.fillMaxWidth()) {
43 43
        Row(Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp)) {
44 44
            Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) {
45 45
                Text("Ready to land", style = MaterialTheme.typography.titleMedium)
…
14 unchanged lines
modified app/src/main/java/io/oxygenhub/android/runner/RunnerCard.kt +2 −2
…
10 unchanged lines
11 11
import androidx.compose.foundation.layout.fillMaxWidth
12 12
import androidx.compose.foundation.layout.padding
13 13
import androidx.compose.material3.Button
14 -
import androidx.compose.material3.ElevatedCard
15 14
import androidx.compose.material3.MaterialTheme
16 15
import androidx.compose.material3.OutlinedButton
17 16
import androidx.compose.material3.Text
…
11 unchanged lines
29 28
import androidx.lifecycle.compose.collectAsStateWithLifecycle
30 29
import io.oxygenhub.android.api.HubApi
31 30
import io.oxygenhub.android.api.userMessage
31 +
import io.oxygenhub.android.ui.common.Panel
32 32
import io.oxygenhub.android.ui.theme.LocalStatusColors
33 33
import kotlinx.coroutines.CancellationException
34 34
import kotlinx.coroutines.launch
…
19 unchanged lines
54 54
        if (granted) PhoneRunner.start(context) else ask.launch(Manifest.permission.POST_NOTIFICATIONS)
55 55
    }
56 56
    val status = LocalStatusColors.current
57 -
    ElevatedCard(Modifier.fillMaxWidth()) {
57 +
    Panel(Modifier.fillMaxWidth()) {
58 58
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
59 59
            Text("OxygenFlow runner", style = MaterialTheme.typography.titleMedium)
60 60
            val (line, colour) = when (val s = state) {
…
46 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/account/AccountScreen.kt +25 −4
…
15 unchanged lines
16 16
import androidx.compose.material.icons.outlined.Dns
17 17
import androidx.compose.material.icons.outlined.Key
18 18
import androidx.compose.material.icons.outlined.Memory
19 +
import androidx.compose.material.icons.outlined.Palette
19 20
import androidx.compose.material3.AlertDialog
20 -
import androidx.compose.material3.Card
21 21
import androidx.compose.material3.CircularProgressIndicator
22 22
import androidx.compose.material3.Icon
23 23
import androidx.compose.material3.ListItem
24 24
import androidx.compose.material3.MaterialTheme
25 25
import androidx.compose.material3.OutlinedButton
26 +
import androidx.compose.material3.Switch
26 27
import androidx.compose.material3.Text
27 28
import androidx.compose.material3.TextButton
28 29
import androidx.compose.runtime.Composable
…
4 unchanged lines
33 34
import androidx.compose.runtime.setValue
34 35
import androidx.compose.ui.Alignment
35 36
import androidx.compose.ui.Modifier
37 +
import androidx.compose.ui.platform.LocalContext
36 38
import androidx.compose.ui.text.font.FontWeight
37 39
import androidx.compose.ui.unit.dp
38 -
import io.oxygenhub.android.auth.Session
40 +
import io.oxygenhub.android.alerts.LandAlertsCard
39 41
import io.oxygenhub.android.api.Avatar
40 42
import io.oxygenhub.android.api.HubApi
41 -
import io.oxygenhub.android.alerts.LandAlertsCard
43 +
import io.oxygenhub.android.auth.Session
42 44
import io.oxygenhub.android.runner.RunnerCard
43 45
import io.oxygenhub.android.ui.common.AppScreen
44 46
import io.oxygenhub.android.ui.common.AvatarImage
47 +
import io.oxygenhub.android.ui.common.Panel
48 +
import io.oxygenhub.android.ui.theme.Appearance
49 +
import io.oxygenhub.android.ui.theme.rememberWallpaperColours
45 50
import io.oxygenhub.android.update.UpdateCard
46 51
import kotlinx.coroutines.launch
47 52
…
19 unchanged lines
67 72
                LandAlertsCard()
68 73
                RunnerCard(api, session.hub, session.canWrite)
69 74
                UpdateCard(api, session.hub)
70 -
                Card(Modifier.fillMaxWidth()) {
75 +
                AppearanceCard()
76 +
                Panel(Modifier.fillMaxWidth()) {
71 77
                    ListItem(
72 78
                        leadingContent = { Icon(Icons.Outlined.Dns, contentDescription = null) },
73 79
                        headlineContent = { Text("Hub") },
…
42 unchanged lines
116 122
                }) { Text("Sign out") }
117 123
            },
118 124
            dismissButton = { TextButton(onClick = { confirming = false }) { Text("Cancel") } },
125 +
        )
126 +
    }
127 +
}
128 +
129 +
/** The app's colours (#24): the hub's palette, or the wallpaper's. */
130 +
@Composable
131 +
private fun AppearanceCard() {
132 +
    val context = LocalContext.current
133 +
    val wallpaper = rememberWallpaperColours()
134 +
    Panel(Modifier.fillMaxWidth()) {
135 +
        ListItem(
136 +
            leadingContent = { Icon(Icons.Outlined.Palette, contentDescription = null) },
137 +
            headlineContent = { Text("Wallpaper colours") },
138 +
            supportingContent = { Text(if (wallpaper) "The app takes its colours from your wallpaper." else "The app uses OxygenHub's own colours, as the website does.") },
139 +
            trailingContent = { Switch(checked = wallpaper, onCheckedChange = { Appearance.setWallpaperColours(context, it) }) },
119 140
        )
120 141
    }
121 142
}
modified app/src/main/java/io/oxygenhub/android/ui/backdrop/Backdrop.kt +3 −0
…
24 unchanged lines
25 25
import androidx.compose.runtime.setValue
26 26
import androidx.compose.ui.Modifier
27 27
import androidx.compose.ui.draw.alpha
28 +
import androidx.compose.ui.draw.clipToBounds
28 29
import androidx.compose.ui.geometry.Offset
29 30
import androidx.compose.ui.graphics.Brush
30 31
import androidx.compose.ui.graphics.Color
…
80 unchanged lines
111 112
112 113
    Box(
113 114
        modifier
115 +
            // A mark near the edge stays in its band (#24), never on the content around it.
116 +
            .clipToBounds()
114 117
            .onSizeChanged { size = it }
115 118
            .pointerInput(scene) {
116 119
                awaitPointerEventScope {
…
140 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/code/CodeBrowser.kt +4 −3
…
18 unchanged lines
19 19
import androidx.compose.material3.AssistChip
20 20
import androidx.compose.material3.DropdownMenu
21 21
import androidx.compose.material3.DropdownMenuItem
22 -
import androidx.compose.material3.ElevatedCard
23 22
import androidx.compose.material3.HorizontalDivider
24 23
import androidx.compose.material3.Icon
25 24
import androidx.compose.material3.ListItem
…
14 unchanged lines
40 39
import io.oxygenhub.android.ui.common.EmptyState
41 40
import io.oxygenhub.android.ui.common.Load
42 41
import io.oxygenhub.android.ui.common.LoadContent
42 +
import io.oxygenhub.android.ui.common.NoImages
43 +
import io.oxygenhub.android.ui.common.Panel
43 44
44 45
/**
45 46
 * A repository's code (#3): where you are (the reference, then each folder
…
31 unchanged lines
77 78
            }
78 79
            dir.readme?.let { readme ->
79 80
                item {
80 -
                    ElevatedCard(Modifier.fillMaxWidth().padding(12.dp)) {
81 +
                    Panel(Modifier.fillMaxWidth().padding(12.dp)) {
81 82
                        Text(
82 83
                            readme.path.substringAfterLast('/'),
83 84
                            style = MaterialTheme.typography.titleSmall,
…
1 unchanged lines
85 86
                        )
86 87
                        Box(Modifier.padding(16.dp)) {
87 88
                            if (readme.path.lowercase().let { it.endsWith(".md") || it.endsWith(".markdown") }) {
88 -
                                Markdown(readme.text)
89 +
                                Markdown(readme.text, imageTransformer = NoImages)
89 90
                            } else {
90 91
                                Text(readme.text, style = MaterialTheme.typography.bodyMedium)
91 92
                            }
…
96 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/code/CodeScreens.kt +4 −3
…
12 unchanged lines
13 13
import androidx.compose.foundation.layout.width
14 14
import androidx.compose.foundation.lazy.LazyColumn
15 15
import androidx.compose.foundation.lazy.LazyListScope
16 +
import androidx.compose.foundation.lazy.items
16 17
import androidx.compose.foundation.lazy.itemsIndexed
17 -
import androidx.compose.foundation.lazy.items
18 18
import androidx.compose.foundation.rememberScrollState
19 19
import androidx.compose.foundation.text.selection.SelectionContainer
20 20
import androidx.compose.material.icons.Icons
…
27 unchanged lines
48 48
import io.oxygenhub.android.api.FileChange
49 49
import io.oxygenhub.android.ui.common.AppScreen
50 50
import io.oxygenhub.android.ui.common.LoadContent
51 +
import io.oxygenhub.android.ui.common.NoImages
51 52
import io.oxygenhub.android.ui.common.relativeTime
52 53
import io.oxygenhub.android.ui.theme.LocalStatusColors
53 54
…
19 unchanged lines
73 74
                    }
74 75
                }
75 76
                if (vm.isMarkdown && rendered) {
76 -
                    LazyColumn { item { Box(Modifier.padding(16.dp)) { Markdown(text) } } }
77 +
                    LazyColumn { item { Box(Modifier.padding(16.dp)) { Markdown(text, imageTransformer = NoImages) } } }
77 78
                } else {
78 79
                    CodeView(text, vm.path)
79 80
                }
…
81 unchanged lines
161 162
                            color = MaterialTheme.colorScheme.onSurfaceVariant,
162 163
                        )
163 164
                        val rest = d.change.message.lines().drop(1).joinToString("\n").trim()
164 -
                        if (rest.isNotEmpty()) Markdown(rest)
165 +
                        if (rest.isNotEmpty()) Markdown(rest, imageTransformer = NoImages)
165 166
                        val where = d.inBookmarks.map { it } + d.inTags.map { "tag $it" }
166 167
                        if (where.isNotEmpty()) Text("In " + where.joinToString(", "), style = MaterialTheme.typography.bodySmall)
167 168
                    }
…
112 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/dashboard/DashboardScreen.kt +5 −9
…
17 unchanged lines
18 18
import androidx.compose.material.icons.outlined.Inbox
19 19
import androidx.compose.material.icons.outlined.MarkEmailUnread
20 20
import androidx.compose.material.icons.outlined.Search
21 -
import androidx.compose.material3.Card
22 -
import androidx.compose.material3.CardDefaults
23 21
import androidx.compose.material3.HorizontalDivider
24 22
import androidx.compose.material3.Icon
25 23
import androidx.compose.material3.IconButton
…
20 unchanged lines
46 44
import io.oxygenhub.android.ui.common.AvatarImage
47 45
import io.oxygenhub.android.ui.common.Load
48 46
import io.oxygenhub.android.ui.common.LoadContent
47 +
import io.oxygenhub.android.ui.common.Panel
48 +
import io.oxygenhub.android.ui.common.panelColors
49 49
import io.oxygenhub.android.ui.common.relativeTime
50 50
import io.oxygenhub.android.ui.repos.RepositoryRow
51 51
import io.oxygenhub.android.ui.theme.LocalStatusColors
…
83 unchanged lines
135 135
136 136
@Composable
137 137
private fun ProposalCard(title: String, list: List<ProposalBrief>, onOpen: (ProposalBrief) -> Unit) {
138 -
    Card(Modifier.fillMaxWidth(), colors = glass()) {
138 +
    Panel(Modifier.fillMaxWidth(), colors = panelColors(alpha = 0.9f)) {
139 139
        Text(
140 140
            "$title · ${list.size}",
141 141
            style = MaterialTheme.typography.titleMedium,
…
24 unchanged lines
166 166
167 167
@Composable
168 168
private fun Unread(inbox: Load<Inbox>, onInbox: () -> Unit) {
169 -
    Card(Modifier.fillMaxWidth().clickable(onClick = onInbox), colors = glass()) {
169 +
    Panel(Modifier.fillMaxWidth().clickable(onClick = onInbox), colors = panelColors(alpha = 0.9f)) {
170 170
        ListItem(
171 171
            colors = ListItemDefaults.colors(containerColor = Color.Transparent),
172 172
            leadingContent = {
…
19 unchanged lines
192 192
193 193
@Composable
194 194
private fun Recent(d: Dashboard, onOpenRepository: (Repository) -> Unit, onAllRepositories: () -> Unit) {
195 -
    Card(Modifier.fillMaxWidth(), colors = glass()) {
195 +
    Panel(Modifier.fillMaxWidth(), colors = panelColors(alpha = 0.9f)) {
196 196
        Text(
197 197
            if (d.recentAreYours) "Your recent repositories" else "Recently updated on the hub",
198 198
            style = MaterialTheme.typography.titleMedium,
…
17 unchanged lines
216 216
        TextButton(onClick = onAllRepositories, modifier = Modifier.padding(8.dp)) { Text("All repositories") }
217 217
    }
218 218
}
219 -
220 -
/** A card the sky shows through a little. */
221 -
@Composable
222 -
private fun glass() = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.8f))
modified app/src/main/java/io/oxygenhub/android/ui/issues/IssueScreen.kt +8 −7
1 1
package io.oxygenhub.android.ui.issues
2 2
3 -
import androidx.compose.material.icons.outlined.PersonAdd
4 -
import androidx.compose.material.icons.outlined.Close
5 -
import androidx.compose.material.icons.outlined.Add
6 3
import androidx.compose.foundation.layout.Arrangement
7 4
import androidx.compose.foundation.layout.Column
8 5
import androidx.compose.foundation.layout.ExperimentalLayoutApi
…
6 unchanged lines
15 12
import androidx.compose.foundation.lazy.items
16 13
import androidx.compose.material.icons.Icons
17 14
import androidx.compose.material.icons.automirrored.outlined.Label
15 +
import androidx.compose.material.icons.outlined.Add
16 +
import androidx.compose.material.icons.outlined.Close
17 +
import androidx.compose.material.icons.outlined.PersonAdd
18 18
import androidx.compose.material3.AlertDialog
19 19
import androidx.compose.material3.Button
20 20
import androidx.compose.material3.Checkbox
21 -
import androidx.compose.material3.ElevatedCard
22 21
import androidx.compose.material3.HorizontalDivider
23 22
import androidx.compose.material3.Icon
24 23
import androidx.compose.material3.IconButton
…
19 unchanged lines
44 43
import io.oxygenhub.android.ui.common.AppScreen
45 44
import io.oxygenhub.android.ui.common.Load
46 45
import io.oxygenhub.android.ui.common.LoadContent
46 +
import io.oxygenhub.android.ui.common.NoImages
47 +
import io.oxygenhub.android.ui.common.Panel
47 48
import io.oxygenhub.android.ui.common.relativeTime
48 49
import io.oxygenhub.android.ui.theme.LocalStatusColors
49 50
…
44 unchanged lines
94 95
                        }
95 96
                        if (i.assignees.isNotEmpty()) Text("Assigned to " + i.assignees.joinToString(", "), style = MaterialTheme.typography.bodySmall)
96 97
                        i.milestone?.let { Text("Milestone: $it", style = MaterialTheme.typography.bodySmall) }
97 -
                        if (i.body.isNotBlank()) Markdown(i.body) else Text("No description.", color = MaterialTheme.colorScheme.onSurfaceVariant)
98 +
                        if (i.body.isNotBlank()) Markdown(i.body, imageTransformer = NoImages) else Text("No description.", color = MaterialTheme.colorScheme.onSurfaceVariant)
98 99
                    }
99 100
                    HorizontalDivider()
100 101
                }
…
21 unchanged lines
122 123
123 124
@Composable
124 125
private fun CommentCard(c: Comment) {
125 -
    ElevatedCard(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp)) {
126 +
    Panel(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp)) {
126 127
        Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
127 128
            Text("${c.author} · ${relativeTime(c.createdMs)}", style = MaterialTheme.typography.labelMedium)
128 -
            Markdown(c.body)
129 +
            Markdown(c.body, imageTransformer = NoImages)
129 130
        }
130 131
    }
131 132
}
…
112 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/projects/ProjectsScreen.kt +3 −3
1 1
package io.oxygenhub.android.ui.projects
2 2
3 3
import androidx.compose.foundation.ExperimentalFoundationApi
4 +
import androidx.compose.foundation.clickable
4 5
import androidx.compose.foundation.combinedClickable
5 -
import androidx.compose.foundation.clickable
6 6
import androidx.compose.foundation.layout.Arrangement
7 7
import androidx.compose.foundation.layout.Box
8 8
import androidx.compose.foundation.layout.Column
…
10 unchanged lines
19 19
import androidx.compose.material.icons.outlined.ViewKanban
20 20
import androidx.compose.material3.DropdownMenu
21 21
import androidx.compose.material3.DropdownMenuItem
22 -
import androidx.compose.material3.ElevatedCard
23 22
import androidx.compose.material3.HorizontalDivider
24 23
import androidx.compose.material3.ListItem
25 24
import androidx.compose.material3.MaterialTheme
…
15 unchanged lines
41 40
import io.oxygenhub.android.ui.common.AppScreen
42 41
import io.oxygenhub.android.ui.common.EmptyState
43 42
import io.oxygenhub.android.ui.common.LoadContent
43 +
import io.oxygenhub.android.ui.common.Panel
44 44
import io.oxygenhub.android.ui.common.relativeTime
45 45
import io.oxygenhub.android.ui.theme.LocalStatusColors
46 46
import kotlinx.coroutines.launch
…
79 unchanged lines
126 126
    var menu by remember { mutableStateOf(false) }
127 127
    val status = LocalStatusColors.current
128 128
    Box {
129 -
        ElevatedCard(
129 +
        Panel(
130 130
            Modifier.fillMaxWidth().padding(horizontal = 12.dp).combinedClickable(
131 131
                onClick = { if (card.kind != "note") onOpen() },
132 132
                onLongClick = { if (canWrite && columns.isNotEmpty()) menu = true },
…
33 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/proposals/ProposalScreen.kt +4 −4
1 1
package io.oxygenhub.android.ui.proposals
2 2
3 -
import androidx.compose.material3.OutlinedTextField
4 3
import androidx.compose.foundation.clickable
5 4
import androidx.compose.foundation.layout.Arrangement
6 5
import androidx.compose.foundation.layout.Box
…
20 unchanged lines
27 26
import androidx.compose.material3.AlertDialog
28 27
import androidx.compose.material3.Button
29 28
import androidx.compose.material3.CircularProgressIndicator
30 -
import androidx.compose.material3.ElevatedCard
31 29
import androidx.compose.material3.HorizontalDivider
32 30
import androidx.compose.material3.Icon
33 31
import androidx.compose.material3.IconButton
…
1 unchanged lines
35 33
import androidx.compose.material3.ListItem
36 34
import androidx.compose.material3.MaterialTheme
37 35
import androidx.compose.material3.OutlinedButton
36 +
import androidx.compose.material3.OutlinedTextField
38 37
import androidx.compose.material3.SnackbarHostState
39 38
import androidx.compose.material3.Switch
40 39
import androidx.compose.material3.Text
…
16 unchanged lines
57 56
import io.oxygenhub.android.api.Proposal
58 57
import io.oxygenhub.android.ui.common.AppScreen
59 58
import io.oxygenhub.android.ui.common.LoadContent
59 +
import io.oxygenhub.android.ui.common.Panel
60 60
import io.oxygenhub.android.ui.common.relativeTime
61 61
import io.oxygenhub.android.ui.issues.LabelChip
62 62
import io.oxygenhub.android.ui.theme.LocalStatusColors
…
89 unchanged lines
152 152
    val status = LocalStatusColors.current
153 153
    var confirming by rememberSaveable { mutableStateOf(false) }
154 154
    val working = action == LandAction.Working
155 -
    ElevatedCard(Modifier.fillMaxWidth()) {
155 +
    Panel(Modifier.fillMaxWidth()) {
156 156
        Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
157 157
            when {
158 158
                p.landed != null -> Text(
…
186 unchanged lines
345 345
private fun Section(title: String, content: @Composable () -> Unit) {
346 346
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
347 347
        Text(title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold)
348 -
        ElevatedCard(Modifier.fillMaxWidth()) { content() }
348 +
        Panel(Modifier.fillMaxWidth()) { content() }
349 349
    }
350 350
}
modified app/src/main/java/io/oxygenhub/android/ui/releases/Releases.kt +4 −3
…
13 unchanged lines
14 14
import androidx.compose.material.icons.outlined.Download
15 15
import androidx.compose.material.icons.outlined.NewReleases
16 16
import androidx.compose.material.icons.outlined.Search
17 -
import androidx.compose.material3.ElevatedCard
18 17
import androidx.compose.material3.HorizontalDivider
19 18
import androidx.compose.material3.Icon
20 19
import androidx.compose.material3.ListItem
…
21 unchanged lines
42 41
import io.oxygenhub.android.ui.common.EmptyState
43 42
import io.oxygenhub.android.ui.common.Load
44 43
import io.oxygenhub.android.ui.common.LoadContent
44 +
import io.oxygenhub.android.ui.common.NoImages
45 +
import io.oxygenhub.android.ui.common.Panel
45 46
import io.oxygenhub.android.ui.common.load
46 47
import io.oxygenhub.android.ui.common.relativeTime
47 48
import kotlinx.coroutines.CancellationException
…
54 unchanged lines
102 103
        } else {
103 104
            LazyColumn(Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp)) {
104 105
                items(releases, key = { it.tag }) { r ->
105 -
                    ElevatedCard(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp)) {
106 +
                    Panel(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp)) {
106 107
                        Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
107 108
                            Text(r.title.ifEmpty { r.tag }, style = MaterialTheme.typography.titleMedium)
108 109
                            Text(
…
7 unchanged lines
116 117
                                style = MaterialTheme.typography.labelMedium,
117 118
                                color = MaterialTheme.colorScheme.onSurfaceVariant,
118 119
                            )
119 -
                            if (r.notes.isNotBlank()) Markdown(r.notes)
120 +
                            if (r.notes.isNotBlank()) Markdown(r.notes, imageTransformer = NoImages)
120 121
                            r.assets.forEach { a ->
121 122
                                ListItem(
122 123
                                    modifier = Modifier.clickable { if (a.url.isNotBlank()) uri.openUri(a.url) },
…
57 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/signin/SignInScreen.kt +3 −3
…
20 unchanged lines
21 21
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
22 22
import androidx.compose.material.icons.outlined.Dns
23 23
import androidx.compose.material3.Button
24 -
import androidx.compose.material3.Card
25 24
import androidx.compose.material3.CardDefaults
26 25
import androidx.compose.material3.CircularProgressIndicator
27 26
import androidx.compose.material3.Icon
…
23 unchanged lines
51 50
import io.oxygenhub.android.R
52 51
import io.oxygenhub.android.api.DeviceCode
53 52
import io.oxygenhub.android.ui.backdrop.Backdrop
53 +
import io.oxygenhub.android.ui.common.Panel
54 54
import uniffi.rust_core.Backdrop as Scene
55 55
56 56
@Composable
…
92 unchanged lines
149 149
    val uri = LocalUriHandler.current
150 150
    Text("Approve this code on the hub", style = MaterialTheme.typography.titleLarge, textAlign = TextAlign.Center)
151 151
    Spacer(Modifier.height(16.dp))
152 -
    Card(
153 -
        colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
152 +
    Panel(
153 +
        colors = CardDefaults.outlinedCardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
154 154
        modifier = Modifier.fillMaxWidth(),
155 155
    ) {
156 156
        Text(
…
34 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/theme/Color.kt +8 −0
…
11 unchanged lines
12 12
val DarkInk = Color(0xFFE6ECF7)
13 13
val DarkMute = Color(0xFF8592AD)
14 14
val DarkLine = Color(0xFF1F2B47)
15 +
// A phone's screen shows less than a monitor's between near-blacks (#24):
16 +
// panels and their edges a step above the site's --panel and --line, each
17 +
// container clearly above the one below.
18 +
val DarkPanel = Color(0xFF121B30)
19 +
val DarkPanelMid = Color(0xFF16203A)
20 +
val DarkPanelHigh = Color(0xFF1B2743)
21 +
val DarkPanelHighest = Color(0xFF21304F)
22 +
val DarkEdge = Color(0xFF2C3D63)
15 23
val DarkAccent = Color(0xFF00E0F0)
16 24
val DarkAccent2 = Color(0xFF0072FF)
17 25
val DarkBand = Color(0xFF0B3F9E)
…
21 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/theme/Theme.kt +25 −9
…
10 unchanged lines
11 11
import androidx.compose.runtime.Immutable
12 12
import androidx.compose.runtime.staticCompositionLocalOf
13 13
import androidx.compose.ui.graphics.Color
14 +
import androidx.compose.ui.graphics.lerp
14 15
import androidx.compose.ui.platform.LocalContext
15 16
16 17
private val DarkColorScheme = darkColorScheme(
…
13 unchanged lines
30 31
    surfaceVariant = DarkSurfaceSoft,
31 32
    onSurfaceVariant = DarkMute,
32 33
    surfaceContainerLowest = DarkBackground,
33 -
    surfaceContainerLow = DarkSurface,
34 -
    surfaceContainer = DarkSurface,
35 -
    surfaceContainerHigh = DarkSurfaceSoft,
36 -
    surfaceContainerHighest = DarkSurfaceSoft,
34 +
    surfaceContainerLow = DarkPanel,
35 +
    surfaceContainer = DarkPanelMid,
36 +
    surfaceContainerHigh = DarkPanelHigh,
37 +
    surfaceContainerHighest = DarkPanelHighest,
37 38
    outline = DarkMute,
38 -
    outlineVariant = DarkLine,
39 +
    outlineVariant = DarkEdge,
39 40
    error = DarkBad,
40 41
)
41 42
…
29 unchanged lines
71 72
72 73
val LocalStatusColors = staticCompositionLocalOf { StatusColors(LightOk, LightBad, LightWarn) }
73 74
75 +
/**
76 +
 * Wallpaper colours in the dark, with containers lifted off the background
77 +
 * (#24): Android's dark tones sit a shade apart, which a phone's screen hardly
78 +
 * shows. Each container a clear step up, and edges brighter than the default.
79 +
 */
80 +
internal fun lifted(scheme: ColorScheme): ColorScheme {
81 +
    val step = { f: Float -> lerp(scheme.background, scheme.onSurface, f) }
82 +
    return scheme.copy(
83 +
        surfaceContainerLow = step(0.06f),
84 +
        surfaceContainer = step(0.09f),
85 +
        surfaceContainerHigh = step(0.12f),
86 +
        surfaceContainerHighest = step(0.15f),
87 +
        outlineVariant = step(0.26f),
88 +
    )
89 +
}
90 +
74 91
@Composable
75 92
fun OxygenHubTheme(
76 93
    darkTheme: Boolean = isSystemInDarkTheme(),
77 -
    // Wallpaper colours (Android 12, the app's minimum, has them); the
78 -
    // hub's own palette when turned off.
79 -
    dynamicColor: Boolean = true,
94 +
    // The hub's own palette, unless Account's "Wallpaper colours" is on (#24).
95 +
    dynamicColor: Boolean = rememberWallpaperColours(),
80 96
    content: @Composable () -> Unit,
81 97
) {
82 98
    val colorScheme: ColorScheme = when {
83 99
        dynamicColor -> {
84 100
            val context = LocalContext.current
85 -
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
101 +
            if (darkTheme) lifted(dynamicDarkColorScheme(context)) else dynamicLightColorScheme(context)
86 102
        }
87 103
        darkTheme -> DarkColorScheme
88 104
        else -> LightColorScheme
…
6 unchanged lines
modified app/src/main/java/io/oxygenhub/android/update/AppUpdate.kt +2 −2
…
2 unchanged lines
3 3
import androidx.compose.foundation.layout.fillMaxWidth
4 4
import androidx.compose.material.icons.Icons
5 5
import androidx.compose.material.icons.outlined.SystemUpdate
6 -
import androidx.compose.material3.Card
7 6
import androidx.compose.material3.Icon
8 7
import androidx.compose.material3.ListItem
9 8
import androidx.compose.material3.Text
…
7 unchanged lines
17 16
import androidx.compose.ui.platform.LocalUriHandler
18 17
import io.oxygenhub.android.api.HubApi
19 18
import io.oxygenhub.android.api.Release
19 +
import io.oxygenhub.android.ui.common.Panel
20 20
21 21
/** Where the app's own releases are (#7), built and signed by OxygenFlow. */
22 22
const val APP_HUB = "https://oxygenhub.io"
…
45 unchanged lines
68 68
        }
69 69
    }
70 70
    val uri = LocalUriHandler.current
71 -
    Card(Modifier.fillMaxWidth()) {
71 +
    Panel(Modifier.fillMaxWidth()) {
72 72
        ListItem(
73 73
            leadingContent = { Icon(Icons.Outlined.SystemUpdate, contentDescription = null) },
74 74
            headlineContent = { Text(update?.let { "Version ${it.version} is out" } ?: "OxygenHub $installed") },
…
7 unchanged lines