OXYGENHUB

Colour on icons: what each stands for, in the hub's colours (closes #25) d98e5a147428

A Tones object in the theme: the accent for repositories, folders and account details; purple for releases, projects and pending checks; the warning colour for private and internal; notifications by kind (broken builds red, fixed green, reviews purple); empty screens' icons in the accent.
Author
Andy McCall @andymccall <[email protected]>
via
agent agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key ece019235b84
When
· 2026-10-28 16:37 UTC
Signature
◈ sealed by agent:claude@andymccall
Hash
d98e5a147428ce02147f8b87d97497550562a408f1c0aa8dc40dca143f993cae
Depends on
144cbf7bf2b5 57c0824300db 62429abe5869 840c75813f86 98dbd1220929 dce8ebd2482a dfbc3e972872 e0004d61958c e2df27a81e5d
Depended on by
b610bc6eeab7 821035db5a1c
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
47 atoms, 1 tree operation, 13 files

Browse the tree at this change

added app/src/main/java/io/oxygenhub/android/ui/theme/Tones.kt +36 −0
1 +
package io.oxygenhub.android.ui.theme
2 +
3 +
import androidx.compose.material3.MaterialTheme
4 +
import androidx.compose.runtime.Composable
5 +
import androidx.compose.ui.graphics.Color
6 +
7 +
/**
8 +
 * What an icon stands for, in the hub's colours (#25), as its pages tint
9 +
 * theirs: the accent for things you open (repositories, folders, account
10 +
 * details), purple for releases, projects and pending things, the status
11 +
 * colours for what passed, failed or is open. Each reads on both palettes.
12 +
 */
13 +
object Tones {
14 +
    val repository: Color @Composable get() = MaterialTheme.colorScheme.primary
15 +
    val folder: Color @Composable get() = MaterialTheme.colorScheme.primary
16 +
    val info: Color @Composable get() = MaterialTheme.colorScheme.primary
17 +
    val release: Color @Composable get() = MaterialTheme.colorScheme.tertiary
18 +
    val project: Color @Composable get() = MaterialTheme.colorScheme.tertiary
19 +
    val pending: Color @Composable get() = MaterialTheme.colorScheme.tertiary
20 +
    val restricted: Color @Composable get() = LocalStatusColors.current.warn
21 +
    val plain: Color @Composable get() = MaterialTheme.colorScheme.onSurfaceVariant
22 +
23 +
    /** A notification's kind (the hub's `notify` kinds). */
24 +
    @Composable
25 +
    fun of(kind: String): Color {
26 +
        val s = LocalStatusColors.current
27 +
        return when {
28 +
            kind == "flow.failed" || kind == "backup" -> s.bad
29 +
            kind == "flow.fixed" || kind == "backup.recovered" -> s.ok
30 +
            kind.startsWith("proposal.review") -> MaterialTheme.colorScheme.tertiary
31 +
            kind.endsWith(".comment") || kind == "mention" -> MaterialTheme.colorScheme.primary
32 +
            kind.startsWith("proposal") || kind.startsWith("issue") -> s.ok
33 +
            else -> MaterialTheme.colorScheme.primary
34 +
        }
35 +
    }
36 +
}
modified CHANGELOG.md +1 −0
…
2 unchanged lines
3 3
## Unreleased
4 4
5 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 +
- **Colour on icons (#25):** icons say what they are in the hub's colours, as its pages do: repositories, folders and account details in the accent, releases, projects and pending checks in purple, private and internal locks in their warning colour, notifications by kind (a broken build red, a fixed one green, a review purple), and empty screens' icons in the accent.
6 7
7 8
## 0.1.0 (2026-10-28)
8 9
…
25 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/account/AccountScreen.kt +5 −4
…
45 unchanged lines
46 46
import io.oxygenhub.android.ui.common.AvatarImage
47 47
import io.oxygenhub.android.ui.common.Panel
48 48
import io.oxygenhub.android.ui.theme.Appearance
49 +
import io.oxygenhub.android.ui.theme.Tones
49 50
import io.oxygenhub.android.ui.theme.rememberWallpaperColours
50 51
import io.oxygenhub.android.update.UpdateCard
51 52
import kotlinx.coroutines.launch
…
23 unchanged lines
75 76
                AppearanceCard()
76 77
                Panel(Modifier.fillMaxWidth()) {
77 78
                    ListItem(
78 -
                        leadingContent = { Icon(Icons.Outlined.Dns, contentDescription = null) },
79 +
                        leadingContent = { Icon(Icons.Outlined.Dns, contentDescription = null, tint = Tones.info) },
79 80
                        headlineContent = { Text("Hub") },
80 81
                        supportingContent = { Text(session.hub) },
81 82
                    )
82 83
                    ListItem(
83 -
                        leadingContent = { Icon(Icons.Outlined.Key, contentDescription = null) },
84 +
                        leadingContent = { Icon(Icons.Outlined.Key, contentDescription = null, tint = Tones.info) },
84 85
                        headlineContent = { Text("Allowed") },
85 86
                        supportingContent = { Text(session.scopes.ifEmpty { listOf("(the hub did not say)") }.joinToString(", ")) },
86 87
                    )
87 88
                    ListItem(
88 -
                        leadingContent = { Icon(Icons.Outlined.Memory, contentDescription = null) },
89 +
                        leadingContent = { Icon(Icons.Outlined.Memory, contentDescription = null, tint = Tones.info) },
89 90
                        headlineContent = { Text("Native core") },
90 91
                        supportingContent = { Text(core) },
91 92
                    )
…
41 unchanged lines
133 134
    val wallpaper = rememberWallpaperColours()
134 135
    Panel(Modifier.fillMaxWidth()) {
135 136
        ListItem(
136 -
            leadingContent = { Icon(Icons.Outlined.Palette, contentDescription = null) },
137 +
            leadingContent = { Icon(Icons.Outlined.Palette, contentDescription = null, tint = Tones.info) },
137 138
            headlineContent = { Text("Wallpaper colours") },
138 139
            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 140
            trailingContent = { Switch(checked = wallpaper, onCheckedChange = { Appearance.setWallpaperColours(context, it) }) },
…
3 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/code/CodeBrowser.kt +4 −3
…
40 unchanged lines
41 41
import io.oxygenhub.android.ui.common.LoadContent
42 42
import io.oxygenhub.android.ui.common.NoImages
43 43
import io.oxygenhub.android.ui.common.Panel
44 +
import io.oxygenhub.android.ui.theme.Tones
44 45
45 46
/**
46 47
 * A repository's code (#3): where you are (the reference, then each folder
…
18 unchanged lines
65 66
                item {
66 67
                    ListItem(
67 68
                        modifier = Modifier.clickable { vm.up() },
68 -
                        leadingContent = { Icon(Icons.Outlined.Folder, contentDescription = null) },
69 +
                        leadingContent = { Icon(Icons.Outlined.Folder, contentDescription = null, tint = Tones.folder) },
69 70
                        headlineContent = { Text("..") },
70 71
                    )
71 72
                }
…
77 unchanged lines
149 150
            TextButton(onClick = { vm.open(parts.take(i + 1).joinToString("/")) }) { Text(part) }
150 151
        }
151 152
        TextButton(onClick = { onOpenHistory(ref) }) {
152 -
            Icon(Icons.Outlined.History, contentDescription = null)
153 +
            Icon(Icons.Outlined.History, contentDescription = null, tint = Tones.info)
153 154
            Text(" History")
154 155
        }
155 156
    }
…
7 unchanged lines
163 164
            Icon(
164 165
                if (e.isDir) Icons.Outlined.Folder else Icons.AutoMirrored.Outlined.InsertDriveFile,
165 166
                contentDescription = if (e.isDir) "Folder" else "File",
166 -
                tint = if (e.isDir) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
167 +
                tint = if (e.isDir) Tones.folder else Tones.plain,
167 168
            )
168 169
        },
169 170
        headlineContent = { Text(e.name) },
…
19 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/common/States.kt +2 −1
…
24 unchanged lines
25 25
import androidx.compose.ui.text.style.TextAlign
26 26
import androidx.compose.ui.unit.dp
27 27
import io.oxygenhub.android.ui.backdrop.Backdrop
28 +
import io.oxygenhub.android.ui.theme.Tones
28 29
import uniffi.rust_core.Backdrop as Scene
29 30
30 31
/** A spinner in the middle of the space. */
…
45 unchanged lines
76 77
            horizontalAlignment = Alignment.CenterHorizontally,
77 78
            verticalArrangement = Arrangement.Center,
78 79
        ) {
79 -
            Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant)
80 +
            Icon(icon, contentDescription = null, modifier = Modifier.size(48.dp), tint = Tones.info.copy(alpha = 0.85f))
80 81
            Spacer(Modifier.height(16.dp))
81 82
            Text(title, style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center)
82 83
            Spacer(Modifier.height(8.dp))
…
30 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/dashboard/DashboardScreen.kt +2 −1
…
48 unchanged lines
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
52 +
import io.oxygenhub.android.ui.theme.Tones
52 53
53 54
@Composable
54 55
fun DashboardScreen(
…
115 unchanged lines
170 171
        ListItem(
171 172
            colors = ListItemDefaults.colors(containerColor = Color.Transparent),
172 173
            leadingContent = {
173 -
                Icon(if ((inbox.valueOrNull?.unread ?: 0) > 0) Icons.Outlined.MarkEmailUnread else Icons.Outlined.Inbox, contentDescription = null)
174 +
                Icon(if ((inbox.valueOrNull?.unread ?: 0) > 0) Icons.Outlined.MarkEmailUnread else Icons.Outlined.Inbox, contentDescription = null, tint = Tones.info)
174 175
            },
175 176
            headlineContent = {
176 177
                Text(
…
42 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/inbox/InboxScreen.kt +4 −3
…
7 unchanged lines
8 8
import androidx.compose.foundation.lazy.items
9 9
import androidx.compose.foundation.shape.CircleShape
10 10
import androidx.compose.material.icons.Icons
11 +
import androidx.compose.material.icons.automirrored.outlined.CallMerge
11 12
import androidx.compose.material.icons.outlined.AlternateEmail
13 +
import androidx.compose.material.icons.outlined.Backup
12 14
import androidx.compose.material.icons.outlined.BugReport
13 -
import androidx.compose.material.icons.automirrored.outlined.CallMerge
14 -
import androidx.compose.material.icons.outlined.Backup
15 15
import androidx.compose.material.icons.outlined.ChatBubbleOutline
16 16
import androidx.compose.material.icons.outlined.DoneAll
17 17
import androidx.compose.material.icons.outlined.Inbox
…
25 unchanged lines
43 43
import io.oxygenhub.android.ui.common.EmptyState
44 44
import io.oxygenhub.android.ui.common.LoadContent
45 45
import io.oxygenhub.android.ui.common.relativeTime
46 +
import io.oxygenhub.android.ui.theme.Tones
46 47
47 48
/**
48 49
 * The inbox. A notification opens what it is about on its own screen in
…
47 unchanged lines
96 97
        colors = if (n.read) ListItemDefaults.colors() else ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainerHigh),
97 98
        leadingContent = {
98 99
            Box(contentAlignment = Alignment.TopEnd) {
99 -
                Icon(iconFor(n.kind), contentDescription = null)
100 +
                Icon(iconFor(n.kind), contentDescription = null, tint = Tones.of(n.kind))
100 101
                if (!n.read) {
101 102
                    Surface(Modifier.size(8.dp), shape = CircleShape, color = MaterialTheme.colorScheme.primary) {}
102 103
                }
…
46 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/issues/IssuesScreen.kt +3 −2
…
13 unchanged lines
14 14
import androidx.compose.foundation.rememberScrollState
15 15
import androidx.compose.foundation.shape.RoundedCornerShape
16 16
import androidx.compose.material.icons.Icons
17 +
import androidx.compose.material.icons.outlined.ChatBubbleOutline
17 18
import androidx.compose.material.icons.outlined.CheckCircle
18 -
import androidx.compose.material.icons.outlined.ChatBubbleOutline
19 19
import androidx.compose.material.icons.outlined.PushPin
20 20
import androidx.compose.material.icons.outlined.RadioButtonUnchecked
21 21
import androidx.compose.material.icons.outlined.TaskAlt
…
22 unchanged lines
44 44
import io.oxygenhub.android.ui.common.LoadContent
45 45
import io.oxygenhub.android.ui.common.relativeTime
46 46
import io.oxygenhub.android.ui.theme.LocalStatusColors
47 +
import io.oxygenhub.android.ui.theme.Tones
47 48
48 49
/**
49 50
 * A repository's issues with their filter, for the repository's tab; an
…
54 unchanged lines
104 105
        overlineContent = if (issue.pinned) {
105 106
            {
106 107
                Row(verticalAlignment = Alignment.CenterVertically) {
107 -
                    Icon(Icons.Outlined.PushPin, contentDescription = null, modifier = Modifier.padding(end = 4.dp))
108 +
                    Icon(Icons.Outlined.PushPin, contentDescription = null, modifier = Modifier.padding(end = 4.dp), tint = Tones.info)
108 109
                    Text("Pinned")
109 110
                }
110 111
            }
…
51 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/owners/Owner.kt +5 −4
…
33 unchanged lines
34 34
import io.oxygenhub.android.api.Visibility
35 35
import io.oxygenhub.android.ui.common.AppScreen
36 36
import io.oxygenhub.android.ui.common.AvatarImage
37 +
import io.oxygenhub.android.ui.common.Load
37 38
import io.oxygenhub.android.ui.common.LoadContent
38 -
import io.oxygenhub.android.ui.common.Load
39 39
import io.oxygenhub.android.ui.common.load
40 +
import io.oxygenhub.android.ui.common.relativeTime
40 41
import io.oxygenhub.android.ui.common.rememberAvatar
41 -
import io.oxygenhub.android.ui.common.relativeTime
42 +
import io.oxygenhub.android.ui.theme.Tones
42 43
import kotlinx.coroutines.Job
43 44
import kotlinx.coroutines.async
44 45
import kotlinx.coroutines.coroutineScope
…
83 unchanged lines
128 129
                    items(page.projects, key = { "p${it.number}" }) { p ->
129 130
                        ListItem(
130 131
                            modifier = Modifier.clickable { onOpenProject(p) },
131 -
                            leadingContent = { Icon(Icons.Outlined.ViewKanban, contentDescription = null) },
132 +
                            leadingContent = { Icon(Icons.Outlined.ViewKanban, contentDescription = null, tint = Tones.project) },
132 133
                            headlineContent = { Text(p.title.ifBlank { "Project ${p.number}" }) },
133 134
                            supportingContent = if (p.description.isNotBlank()) {
134 135
                                { Text(p.description, maxLines = 2) }
…
16 unchanged lines
151 152
                            )
152 153
                        },
153 154
                        trailingContent = if (r.visibility != Visibility.Public) {
154 -
                            { Icon(Icons.Outlined.Lock, contentDescription = "Not public") }
155 +
                            { Icon(Icons.Outlined.Lock, contentDescription = "Not public", tint = Tones.restricted) }
155 156
                        } else {
156 157
                            null
157 158
                        },
…
17 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/proposals/ProposalScreen.kt +2 −1
…
59 unchanged lines
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
63 +
import io.oxygenhub.android.ui.theme.Tones
63 64
64 65
/**
65 66
 * One proposal: whether it may land and what stands in the way, in the
…
216 unchanged lines
282 283
            }
283 284
            p.requestedReviewers.forEach { r ->
284 285
                ListItem(
285 -
                    leadingContent = { Icon(Icons.Outlined.Schedule, null) },
286 +
                    leadingContent = { Icon(Icons.Outlined.Schedule, null, tint = Tones.pending) },
286 287
                    headlineContent = { Text(r) },
287 288
                    supportingContent = { Text("asked to review") },
288 289
                )
…
62 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/releases/Releases.kt +2 −1
…
44 unchanged lines
45 45
import io.oxygenhub.android.ui.common.Panel
46 46
import io.oxygenhub.android.ui.common.load
47 47
import io.oxygenhub.android.ui.common.relativeTime
48 +
import io.oxygenhub.android.ui.theme.Tones
48 49
import kotlinx.coroutines.CancellationException
49 50
import kotlinx.coroutines.Job
50 51
import kotlinx.coroutines.flow.MutableStateFlow
…
70 unchanged lines
121 122
                            r.assets.forEach { a ->
122 123
                                ListItem(
123 124
                                    modifier = Modifier.clickable { if (a.url.isNotBlank()) uri.openUri(a.url) },
124 -
                                    leadingContent = { Icon(Icons.Outlined.Download, contentDescription = "Download") },
125 +
                                    leadingContent = { Icon(Icons.Outlined.Download, contentDescription = "Download", tint = Tones.release) },
125 126
                                    headlineContent = { Text(a.name) },
126 127
                                    supportingContent = { Text("${bytes(a.size)} · ${a.downloads} downloads") },
127 128
                                )
…
53 unchanged lines
modified app/src/main/java/io/oxygenhub/android/ui/repos/RepositoryRow.kt +4 −3
…
22 unchanged lines
23 23
import io.oxygenhub.android.api.Repository
24 24
import io.oxygenhub.android.api.Visibility
25 25
import io.oxygenhub.android.ui.common.relativeTime
26 +
import io.oxygenhub.android.ui.theme.Tones
26 27
27 28
@Composable
28 29
fun RepositoryRow(repo: Repository, onClick: () -> Unit, modifier: Modifier = Modifier, selected: Boolean = false) {
…
5 unchanged lines
34 35
            // Transparent: the row takes the colour of what holds it (a card, a pane).
35 36
            ListItemDefaults.colors(containerColor = Color.Transparent)
36 37
        },
37 -
        leadingContent = { Icon(Icons.Outlined.Book, contentDescription = null) },
38 +
        leadingContent = { Icon(Icons.Outlined.Book, contentDescription = null, tint = Tones.repository) },
38 39
        headlineContent = { Text(repo.slug, maxLines = 1, overflow = TextOverflow.Ellipsis) },
39 40
        supportingContent = if (repo.description.isNotBlank()) {
40 41
            { Text(repo.description, maxLines = 2, overflow = TextOverflow.Ellipsis) }
…
3 unchanged lines
44 45
        trailingContent = {
45 46
            Row(verticalAlignment = Alignment.CenterVertically) {
46 47
                when (repo.visibility) {
47 -
                    Visibility.Private -> Icon(Icons.Outlined.Lock, contentDescription = "Private", modifier = Modifier.size(16.dp))
48 -
                    Visibility.Internal -> Icon(Icons.Outlined.Domain, contentDescription = "Internal", modifier = Modifier.size(16.dp))
48 +
                    Visibility.Private -> Icon(Icons.Outlined.Lock, contentDescription = "Private", modifier = Modifier.size(16.dp), tint = Tones.restricted)
49 +
                    Visibility.Internal -> Icon(Icons.Outlined.Domain, contentDescription = "Internal", modifier = Modifier.size(16.dp), tint = Tones.restricted)
49 50
                    Visibility.Public -> {}
50 51
                }
51 52
                if (repo.visibility != Visibility.Public) Spacer(Modifier.width(6.dp))
…
5 unchanged lines
modified app/src/main/java/io/oxygenhub/android/update/AppUpdate.kt +2 −1
…
16 unchanged lines
17 17
import io.oxygenhub.android.api.HubApi
18 18
import io.oxygenhub.android.api.Release
19 19
import io.oxygenhub.android.ui.common.Panel
20 +
import io.oxygenhub.android.ui.theme.Tones
20 21
21 22
/** Where the app's own releases are (#7), built and signed by OxygenFlow. */
22 23
const val APP_HUB = "https://oxygenhub.io"
…
47 unchanged lines
70 71
    val uri = LocalUriHandler.current
71 72
    Panel(Modifier.fillMaxWidth()) {
72 73
        ListItem(
73 -
            leadingContent = { Icon(Icons.Outlined.SystemUpdate, contentDescription = null) },
74 +
            leadingContent = { Icon(Icons.Outlined.SystemUpdate, contentDescription = null, tint = Tones.info) },
74 75
            headlineContent = { Text(update?.let { "Version ${it.version} is out" } ?: "OxygenHub $installed") },
75 76
            supportingContent = {
76 77
                Text(if (update != null) "You have $installed. Download it and open the file to update." else "Releases are at $APP_HUB/$APP_OWNER/$APP_REPO.")
…
5 unchanged lines