From 741c5cea68e6d5b63a9f6333afe3ff128277f1ee Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Wed, 9 Sep 2026 20:58:00 +0200 Subject: [PATCH] accounts: real provider marks, from the vendors' own art MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nextcloud, Fastmail, iCloud, mailbox.org and Posteo wear their own logos instead of one shared @ glyph across six providers. Fastmail and mailbox.org are full-colour badges; the rest are tinted marks on the brand disc. Anything without usable art takes the brand's initial, which for Yandex is its own Я. Posteo's green is its real one now. --- .../agendula/ui/accounts/AccountIdentity.kt | 146 ++++++++++++++---- .../accounts/{ => add}/AddAccountMessage.kt | 0 .../ui/accounts/{ => add}/AddAccountScreen.kt | 0 .../accounts/{ => add}/AddAccountViewModel.kt | 0 .../res/drawable/ic_provider_fastmail.xml | 31 ++++ .../main/res/drawable/ic_provider_icloud.xml | 24 +++ .../main/res/drawable/ic_provider_mailbox.xml | 24 +++ .../res/drawable/ic_provider_nextcloud.xml | 35 +++-- .../main/res/drawable/ic_provider_posteo.xml | 31 ++++ .../{ => add}/AddAccountViewModelTest.kt | 0 10 files changed, 246 insertions(+), 45 deletions(-) rename app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/{ => add}/AddAccountMessage.kt (100%) rename app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/{ => add}/AddAccountScreen.kt (100%) rename app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/{ => add}/AddAccountViewModel.kt (100%) create mode 100644 app/src/main/res/drawable/ic_provider_fastmail.xml create mode 100644 app/src/main/res/drawable/ic_provider_icloud.xml create mode 100644 app/src/main/res/drawable/ic_provider_mailbox.xml create mode 100644 app/src/main/res/drawable/ic_provider_posteo.xml rename app/src/test/java/de/jeanlucmakiola/agendula/ui/accounts/{ => add}/AddAccountViewModelTest.kt (100%) diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AccountIdentity.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AccountIdentity.kt index 7f834de..6506346 100644 --- a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AccountIdentity.kt +++ b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AccountIdentity.kt @@ -1,21 +1,23 @@ package de.jeanlucmakiola.agendula.ui.accounts +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.AlternateEmail -import androidx.compose.material.icons.rounded.Cloud import androidx.compose.material.icons.rounded.CloudSync -import androidx.compose.material.icons.rounded.Dns import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -65,11 +67,52 @@ internal fun AccountEntity.identity(): AccountIdentity { * White-on-brand rather than a brand-tinted glyph on a neutral chip, because it * is the shape the marks are actually drawn in and it is the one treatment that * needs no second colour for dark mode. Providers with no brand colour of their - * own take the app's [MaterialTheme] accent and say what they are — a mail - * service, a server, a cloud. + * own take the app's [MaterialTheme] accent. + * + * There are three treatments, and which one a provider gets is decided by how + * that provider actually draws itself: + * + * 1. **A badge** — [CalDavProvider.badge] — fills the circle edge to edge in its + * own colours. Fastmail's icon *is* a ring, so a disc behind it would be a + * ring inside a circle, and knocking it back to white would throw away the + * logo's larger half. + * 2. **A mark** — [CalDavProvider.mark] — is tinted white on the brand's disc, + * which is how Nextcloud, Apple and Posteo draw these marks themselves. + * 3. **A lettermark** for everything else. Material's `AlternateEmail` was doing + * duty for six providers at once, so a list meant to be read by mark showed + * one glyph six times; the brand's own initial tells them apart and claims + * nothing. The case comes from [CalDavProvider.label], which is why iCloud + * and mailbox.org keep their lowercase letterforms. + * + * A real logo beats a letter; an *approximated* logo beats neither, which is why + * the rest wait for their own art rather than for a good guess at it — and why + * every mark here is generated from the vendor's own file, not traced by eye. */ @Composable internal fun ProviderLogo(provider: CalDavProvider?, size: Dp = 40.dp) { + val badge = provider?.badge + if (badge != null) { + // ⚠️ On white, not on nothing. The badge is a ring with a transparent + // middle, drawn for a white page — dropped straight onto the row it lets + // the surface through, and in dark mode the navy envelope inside it goes + // very nearly invisible. White is the background the art is drawn for, so + // it is the background it gets, in both themes. + Box( + modifier = Modifier + .size(size) + .clip(CircleShape) + .background(Color.White), + contentAlignment = Alignment.Center, + ) { + Image( + painter = painterResource(badge), + contentDescription = null, + modifier = Modifier.size(size), + ) + } + return + } + val accent = provider?.accent Box( modifier = Modifier @@ -79,36 +122,75 @@ internal fun ProviderLogo(provider: CalDavProvider?, size: Dp = 40.dp) { contentAlignment = Alignment.Center, ) { val tint = if (accent != null) Color.White else MaterialTheme.colorScheme.onPrimary - val glyph = Modifier.size(size * GLYPH_FRACTION) - if (provider == CalDavProvider.NEXTCLOUD) { - Icon( - painter = painterResource(R.drawable.ic_provider_nextcloud), - contentDescription = provider.label, + val mark = provider?.mark + when { + mark != null -> Icon( + painter = painterResource(mark.res), + contentDescription = null, tint = tint, - modifier = glyph, + modifier = Modifier.size(size * mark.fraction), ) - } else { - Icon( - imageVector = when (provider) { - CalDavProvider.BAIKAL, - CalDavProvider.DAVICAL, - CalDavProvider.SOGO, - -> Icons.Rounded.Dns - CalDavProvider.ICLOUD -> Icons.Rounded.Cloud - - null -> Icons.Rounded.CloudSync - - else -> Icons.Rounded.AlternateEmail - }, - contentDescription = provider?.label, + // A server we know nothing about has no initial to wear. + provider == null -> Icon( + imageVector = Icons.Rounded.CloudSync, + contentDescription = null, tint = tint, - modifier = glyph, + modifier = Modifier.size(size * GLYPH_FRACTION), + ) + + else -> Text( + text = provider.letter, + color = tint, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + fontSize = with(LocalDensity.current) { (size * LETTER_FRACTION).toSp() }, + // The name is on the row beside it. A screen reader announcing a + // bare "F" before "Fastmail" is noise, not information. + modifier = Modifier.clearAndSetSemantics { }, ) } } } +/** A provider whose official icon is a finished badge, colours and all. */ +private val CalDavProvider.badge: Int? + get() = when (this) { + CalDavProvider.FASTMAIL -> R.drawable.ic_provider_fastmail + CalDavProvider.MAILBOX_ORG -> R.drawable.ic_provider_mailbox + else -> null + } + +/** + * The letter a service is known by. + * + * Its own initial, except where the service's actual mark *is* a different + * letter: Yandex's is a Cyrillic Я, which is something we can set rather than + * art we would have to trace — the only vector they publish is a 64px raster. + */ +private val CalDavProvider.letter: String + get() = when (this) { + CalDavProvider.YANDEX -> "Я" + else -> label.take(1) + } + +/** + * A monochrome mark and how much of the disc it is given. + * + * The fraction is not one number because the marks are not one shape: a wide + * mark squared off into the same box reads smaller than a compact one, so it is + * given more room to land on the same optical weight. + */ +private data class Mark(val res: Int, val fraction: Float) + +private val CalDavProvider.mark: Mark? + get() = when (this) { + CalDavProvider.NEXTCLOUD -> Mark(R.drawable.ic_provider_nextcloud, WIDE_MARK_FRACTION) + CalDavProvider.ICLOUD -> Mark(R.drawable.ic_provider_icloud, WIDE_MARK_FRACTION) + CalDavProvider.POSTEO -> Mark(R.drawable.ic_provider_posteo, GLYPH_FRACTION) + else -> null + } + /** * The provider's own brand colour, where it publishes one recognisable enough to * be worth carrying. Null means "we would be inventing it" — Baïkal, DAViCal and @@ -125,11 +207,19 @@ private val CalDavProvider.accent: Color? CalDavProvider.GOOGLE -> Color(0xFF1A73E8) CalDavProvider.FASTMAIL -> Color(0xFF2B6CB0) CalDavProvider.MAILBOX_ORG -> Color(0xFF0069B4) - CalDavProvider.POSTEO -> Color(0xFF5E9B23) + // Their own, off their app icon — not the darker green that was + // guessed at before the art arrived. + CalDavProvider.POSTEO -> Color(0xFFA9D158) CalDavProvider.ZOHO -> Color(0xFFE42527) - CalDavProvider.YANDEX -> Color(0xFFE03A1B) + CalDavProvider.YANDEX -> Color(0xFFFF2500) CalDavProvider.BAIKAL, CalDavProvider.DAVICAL, CalDavProvider.SOGO -> null } /** The mark sits on the disc the way a launcher icon does — a little over half. */ private const val GLYPH_FRACTION = 0.55f + +/** A letter reads smaller than a glyph of the same box, so it is given less. */ +private const val LETTER_FRACTION = 0.44f + +/** A wide mark squared off into the same box has to be given more to match. */ +private const val WIDE_MARK_FRACTION = 0.72f diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountMessage.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountMessage.kt similarity index 100% rename from app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountMessage.kt rename to app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountMessage.kt diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountScreen.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountScreen.kt similarity index 100% rename from app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountScreen.kt rename to app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountScreen.kt diff --git a/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModel.kt b/app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountViewModel.kt similarity index 100% rename from app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModel.kt rename to app/src/main/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountViewModel.kt diff --git a/app/src/main/res/drawable/ic_provider_fastmail.xml b/app/src/main/res/drawable/ic_provider_fastmail.xml new file mode 100644 index 0000000..c0f151a --- /dev/null +++ b/app/src/main/res/drawable/ic_provider_fastmail.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/ic_provider_icloud.xml b/app/src/main/res/drawable/ic_provider_icloud.xml new file mode 100644 index 0000000..7d55522 --- /dev/null +++ b/app/src/main/res/drawable/ic_provider_icloud.xml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_provider_mailbox.xml b/app/src/main/res/drawable/ic_provider_mailbox.xml new file mode 100644 index 0000000..afec60c --- /dev/null +++ b/app/src/main/res/drawable/ic_provider_mailbox.xml @@ -0,0 +1,24 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_provider_nextcloud.xml b/app/src/main/res/drawable/ic_provider_nextcloud.xml index 7c089d9..d069e5a 100644 --- a/app/src/main/res/drawable/ic_provider_nextcloud.xml +++ b/app/src/main/res/drawable/ic_provider_nextcloud.xml @@ -1,24 +1,25 @@ - - - + android:viewportWidth="150" + android:viewportHeight="150"> + + + diff --git a/app/src/main/res/drawable/ic_provider_posteo.xml b/app/src/main/res/drawable/ic_provider_posteo.xml new file mode 100644 index 0000000..2603095 --- /dev/null +++ b/app/src/main/res/drawable/ic_provider_posteo.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/app/src/test/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModelTest.kt b/app/src/test/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountViewModelTest.kt similarity index 100% rename from app/src/test/java/de/jeanlucmakiola/agendula/ui/accounts/AddAccountViewModelTest.kt rename to app/src/test/java/de/jeanlucmakiola/agendula/ui/accounts/add/AddAccountViewModelTest.kt