From 6d7b6483c0d54dc88d4bfd0105dafa1c04d7ab3a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 09:47:23 +0000 Subject: [PATCH] =?UTF-8?q?feat(mail):=20Seitenleiste=20mit=20neuem=20Fu?= =?UTF-8?q?=C3=9F=20(Einstellungen=20links,=20Kontakte/Kalender=20rechts)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fuß der Mail-Seitenleiste wie Mockup „Seitenmenü neu“: links das Zahnrad (Mail-Einstellungen), rechtsbündig Kontakte und Kalender (je 44×44, aktiver Bereich im Akzent). Beide erscheinen nur mit features.contacts bzw. features.calendar und sobald die App den Bereich öffnen kann (folgt mit Kalender und Kontakten). - Das „+“ entfällt: ein Konto kommt nur noch über die Mail-Einstellungen dazu. „Konto hinzufügen“ im Leer-Zustand öffnet die Mail-Einstellungen mit „Konto hinzufügen“ darüber (Zurück führt in die Einstellungen). - Texte DE/EN, Unit-Test für die Feature-Abfrage. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016xX1efcZF1f6G9rhmaJNLD --- .../gatecontrol/android/mail/MailModels.kt | 3 + .../android/navigation/MailNavigation.kt | 6 +- .../android/ui/mail/MailHomeScreen.kt | 70 +++++++++++++++---- app/src/main/res/values-de/strings_mail.xml | 2 + app/src/main/res/values/strings_mail.xml | 2 + .../android/ui/mail/MailDrawerFootTest.kt | 32 +++++++++ 6 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 app/src/test/java/com/gatecontrol/android/ui/mail/MailDrawerFootTest.kt diff --git a/app/src/main/java/com/gatecontrol/android/mail/MailModels.kt b/app/src/main/java/com/gatecontrol/android/mail/MailModels.kt index 29980096..32c14285 100644 --- a/app/src/main/java/com/gatecontrol/android/mail/MailModels.kt +++ b/app/src/main/java/com/gatecontrol/android/mail/MailModels.kt @@ -44,6 +44,9 @@ data class MMe( const val FEATURE_DRIVE = "drive" const val FEATURE_FAMILY_CALENDAR = "familyCalendar" const val FEATURE_IMAGES = "images" + /** Since Mail 1.2.0: calendars and contacts (the administrator switched them on). */ + const val FEATURE_CALENDAR = "calendar" + const val FEATURE_CONTACTS = "contacts" } } diff --git a/app/src/main/java/com/gatecontrol/android/navigation/MailNavigation.kt b/app/src/main/java/com/gatecontrol/android/navigation/MailNavigation.kt index 9ef61f6b..e5a28046 100644 --- a/app/src/main/java/com/gatecontrol/android/navigation/MailNavigation.kt +++ b/app/src/main/java/com/gatecontrol/android/navigation/MailNavigation.kt @@ -30,7 +30,11 @@ private class MailNavigation(private val navController: NavHostController) { draft = { go(Screen.MailCompose.create(draft = it)) }, search = { go(Screen.MailSearch.route) }, filing = { go(Screen.MailFiling.route) }, - addAccount = { go(Screen.MailAddAccount.create()) }, + addAccount = { + // an account is added in the settings: back from adding leads there + go(Screen.MailSettings.route) + go(Screen.MailAddAccount.create()) + }, settings = { go(Screen.MailSettings.route) }, account = { go(Screen.MailAccount.create(it)) }, outbox = { go(Screen.MailOutbox.route) }, diff --git a/app/src/main/java/com/gatecontrol/android/ui/mail/MailHomeScreen.kt b/app/src/main/java/com/gatecontrol/android/ui/mail/MailHomeScreen.kt index 0740eb47..5b87d81d 100644 --- a/app/src/main/java/com/gatecontrol/android/ui/mail/MailHomeScreen.kt +++ b/app/src/main/java/com/gatecontrol/android/ui/mail/MailHomeScreen.kt @@ -90,6 +90,7 @@ data class MailNav( val draft: (Long) -> Unit, val search: () -> Unit, val filing: () -> Unit, + /** "Konto hinzufügen" of the empty tab: the settings with "Konto hinzufügen" on top (the menu has no "+"). */ val addAccount: () -> Unit, val settings: () -> Unit, val account: (Long) -> Unit, @@ -172,7 +173,6 @@ fun MailHomeScreen(nav: MailNav, viewModel: MailHomeViewModel = hiltViewModel()) onBox = { close(); viewModel.selectBox(it) }, onFiling = { close(); nav.filing() }, onOutbox = { close(); nav.outbox() }, - onAdd = { close(); nav.addAccount() }, onSettings = { close(); nav.settings() }, ) } @@ -576,10 +576,11 @@ internal object MailFolders { } /** - * The menu (mockup "Seitenmenü"): "Mail", "Alle Posteingänge", "Zum Ablegen" (with Drive), "Postausgang" (when - * something waits), the accounts with their folders and below "Konto hinzufügen" and the settings. A tap on an - * account opens or closes its folders (the menu stays, [folds] keeps it for the session); picking its "Posteingang" - * or a folder closes the menu. Calendar and resources come later (phase 2/3). + * The menu (mockups "Seitenmenü" and "Seitenmenü neu"): "Mail", "Alle Posteingänge", "Zum Ablegen" (with Drive), + * "Postausgang" (when something waits), the accounts with their folders, the storage bar and the foot: the settings + * on the left, contacts and calendar on the right ([MailDrawerFoot]). A tap on an account opens or closes its folders + * (the menu stays, [folds] keeps it for the session); picking its "Posteingang" or a folder closes the menu. An + * account is added in the settings only (no "+" here since phase 2). */ @Composable private fun MailDrawer( @@ -593,8 +594,10 @@ private fun MailDrawer( onBox: (MailLogic.Box) -> Unit, onFiling: () -> Unit, onOutbox: () -> Unit, - onAdd: () -> Unit, onSettings: () -> Unit, + onContacts: (() -> Unit)? = null, + onCalendar: (() -> Unit)? = null, + area: String = MailDrawerFoot.MAIL, ) { val extra = GateControlTheme.extraColors Column(Modifier.fillMaxHeight().padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 16.dp)) { @@ -653,13 +656,50 @@ private fun MailDrawer( } } } - Row(Modifier.padding(horizontal = 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { - DrawerIcon(GcIcons.Plus, stringResource(R.string.mail_add_account), onAdd) - DrawerIcon(GcIcons.Settings, stringResource(R.string.mail_settings), onSettings) + MailDrawerFoot(me = me, area = area, onSettings = onSettings, onContacts = onContacts, onCalendar = onCalendar) + } +} + +/** + * The menu's foot (mockup "Seitenmenü neu"): the settings on the left; contacts and calendar on the right, each only + * when the server offers it (`features.contacts` / `features.calendar`) and the app can open it, the active area in the + * accent. A tap on the active one goes back to the mail. + */ +@Composable +internal fun MailDrawerFoot( + me: MMe?, + area: String, + onSettings: () -> Unit, + onContacts: (() -> Unit)?, + onCalendar: (() -> Unit)?, +) { + Row( + Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + DrawerIcon(GcIcons.Settings, stringResource(R.string.mail_settings), on = false, onClick = onSettings) + Spacer(Modifier.weight(1f)) + if (onContacts != null && MailDrawerFoot.contacts(me)) { + DrawerIcon(GcIcons.User, stringResource(R.string.mail_contacts), on = area == MailDrawerFoot.CONTACTS, onClick = onContacts) + } + if (onCalendar != null && MailDrawerFoot.calendar(me)) { + DrawerIcon(GcIcons.Calendar, stringResource(R.string.mail_calendar), on = area == MailDrawerFoot.CALENDAR, onClick = onCalendar) } } } +/** What the menu's foot shows (pure, unit-tested). */ +internal object MailDrawerFoot { + const val MAIL = "mail" + const val CALENDAR = "calendar" + const val CONTACTS = "contacts" + + fun calendar(me: MMe?): Boolean = me?.has(MMe.FEATURE_CALENDAR) == true + + fun contacts(me: MMe?): Boolean = me?.has(MMe.FEATURE_CONTACTS) == true +} + @Composable private fun DrawerLabel(text: String) { Text( @@ -740,18 +780,20 @@ private fun DrawerAccount(a: MAccount, count: Int, on: Boolean, open: Boolean, o } } +/** A square button of the menu's foot (44×44); [on]: the area shown now, in the accent (`aria-pressed`). */ @Composable -private fun DrawerIcon(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String, onClick: () -> Unit) { +private fun DrawerIcon(icon: androidx.compose.ui.graphics.vector.ImageVector, label: String, on: Boolean, onClick: () -> Unit) { val extra = GateControlTheme.extraColors + val shape = RoundedCornerShape(12.dp) Box( Modifier .size(44.dp) - .clip(RoundedCornerShape(12.dp)) - .border(1.dp, extra.border, RoundedCornerShape(12.dp)) - .clickable(role = Role.Button, onClick = onClick) + .clip(shape) + .then(if (on) Modifier.background(extra.accentBg) else Modifier.border(1.dp, extra.border, shape)) + .selectable(selected = on, role = Role.Button, onClick = onClick) .semantics { contentDescription = label }, contentAlignment = Alignment.Center, ) { - Icon(icon, contentDescription = null, tint = extra.muted, modifier = Modifier.size(20.dp)) + Icon(icon, contentDescription = null, tint = if (on) extra.accentText else extra.muted, modifier = Modifier.size(20.dp)) } } diff --git a/app/src/main/res/values-de/strings_mail.xml b/app/src/main/res/values-de/strings_mail.xml index 95153741..083bace8 100644 --- a/app/src/main/res/values-de/strings_mail.xml +++ b/app/src/main/res/values-de/strings_mail.xml @@ -53,6 +53,8 @@ Konten Konto hinzufügen Mail-Einstellungen + Kalender + Kontakte %1$s von %2$s belegt Offline – du siehst den zuletzt geladenen Stand. %1$s: Anmeldung abgelehnt. Tippen, um dich neu anzumelden. diff --git a/app/src/main/res/values/strings_mail.xml b/app/src/main/res/values/strings_mail.xml index 06a6d7d2..639a9dc3 100644 --- a/app/src/main/res/values/strings_mail.xml +++ b/app/src/main/res/values/strings_mail.xml @@ -53,6 +53,8 @@ Accounts Add account Mail settings + Calendar + Contacts %1$s of %2$s used Offline – showing what was loaded last. %1$s: sign-in refused. Tap to sign in again. diff --git a/app/src/test/java/com/gatecontrol/android/ui/mail/MailDrawerFootTest.kt b/app/src/test/java/com/gatecontrol/android/ui/mail/MailDrawerFootTest.kt new file mode 100644 index 00000000..54949d8a --- /dev/null +++ b/app/src/test/java/com/gatecontrol/android/ui/mail/MailDrawerFootTest.kt @@ -0,0 +1,32 @@ +package com.gatecontrol.android.ui.mail + +import com.gatecontrol.android.mail.MLimits +import com.gatecontrol.android.mail.MMe +import com.gatecontrol.android.mail.MMicrosoft +import com.gatecontrol.android.mail.MSettings +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class MailDrawerFootTest { + + private fun me(vararg features: String) = MMe( + features = features.toSet(), + limits = MLimits(5, 0, 0, 12), + microsoft = MMicrosoft(null, "https://login.microsoftonline.com/common", emptyList()), + accounts = emptyList(), + unreadAll = 0, + unreadByAccount = emptyMap(), + settings = MSettings.DEFAULT, + ) + + @Test + fun `contacts and calendar only with the server's features`() { + assertFalse(MailDrawerFoot.calendar(null)) + assertFalse(MailDrawerFoot.contacts(null)) + assertFalse(MailDrawerFoot.calendar(me("mail"))) + assertFalse(MailDrawerFoot.contacts(me("mail", "calendar"))) + assertTrue(MailDrawerFoot.calendar(me("mail", "calendar"))) + assertTrue(MailDrawerFoot.contacts(me("mail", "contacts"))) + } +}