refactor(detail): show availability only when Free, pin it by the title

The Busy availability value is the default for nearly every event, so a
"Busy" chip on every detail screen was noise. Show the pill only for Free
(the noteworthy case) and move it to the top-right of the title row instead
of the under-title chip strip. That strip now carries only status/access and
hides entirely when there's nothing noteworthy. Drops the now-unused
event_availability_busy string from both locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:08:21 +02:00
parent 024512959f
commit dca0245a42
4 changed files with 40 additions and 34 deletions

View File

@@ -16,8 +16,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
"1 day before", "At time of event"), read from `CalendarContract.Reminders` "1 day before", "At time of event"), read from `CalendarContract.Reminders`
- **Status** — Tentative / Cancelled chip under the title; a cancelled event - **Status** — Tentative / Cancelled chip under the title; a cancelled event
also strikes through its title (Confirmed shows no chip) also strikes through its title (Confirmed shows no chip)
- **Availability** — a Free / Busy chip (`Events.AVAILABILITY`, the iCal - **Availability** — a "Free" pill pinned top-right of the title when the
TRANSP field) event doesn't block your time (`Events.AVAILABILITY`, the iCal TRANSP
field); the default "Busy" is left implicit to avoid noise on every event
- **Access level** — a Private / Confidential chip when the event isn't public - **Access level** — a Private / Confidential chip when the event isn't public
- **Attendee role** — organizer / optional / resource badge under each - **Attendee role** — organizer / optional / resource badge under each
attendee, plus the device user's own response ("Your response: …") from attendee, plus the device user's own response ("Your response: …") from

View File

@@ -174,8 +174,11 @@ private fun EventDetailContent(state: EventDetailUiState.Success, modifier: Modi
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
.padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 40.dp), .padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 40.dp),
) { ) {
// Title with a short accent line in the calendar colour underneath. // Title row: title on the left, a "Free" pill pinned top-right when the
// A cancelled event strikes through the title. // event doesn't block your time. Busy is the default for nearly every
// event, so it's left implicit — only Free is worth surfacing. A
// cancelled event strikes through its title.
Row(verticalAlignment = Alignment.Top) {
Text( Text(
text = instance.title.ifBlank { stringResource(R.string.event_untitled) }, text = instance.title.ifBlank { stringResource(R.string.event_untitled) },
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
@@ -185,7 +188,16 @@ private fun EventDetailContent(state: EventDetailUiState.Success, modifier: Modi
} else { } else {
null null
}, },
modifier = Modifier.weight(1f),
) )
if (detail.availability == Availability.Free) {
Spacer(Modifier.width(12.dp))
InfoChip(
text = stringResource(R.string.event_availability_free),
modifier = Modifier.padding(top = 6.dp),
)
}
}
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
Box( Box(
modifier = Modifier modifier = Modifier
@@ -194,10 +206,15 @@ private fun EventDetailContent(state: EventDetailUiState.Success, modifier: Modi
.background(accent, RoundedCornerShape(2.dp)), .background(accent, RoundedCornerShape(2.dp)),
) )
// Status / availability / access chips. Availability is always known, so // Status / access chips — shown only when noteworthy (Confirmed status
// this row always shows at least the Free/Busy chip. // and Default/Public access are the silent norm).
val hasStatusChips = detail.status != EventStatus.Confirmed ||
detail.accessLevel == AccessLevel.Private ||
detail.accessLevel == AccessLevel.Confidential
if (hasStatusChips) {
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
StatusChips(detail.status, detail.availability, detail.accessLevel) StatusChips(detail.status, detail.accessLevel)
}
Spacer(Modifier.height(20.dp)) Spacer(Modifier.height(20.dp))
@@ -399,14 +416,10 @@ private fun AttendeeRow(attendee: Attendee) {
} }
} }
/** Status / availability / access pills shown directly under the title accent. */ /** Status / access pills shown directly under the title accent. */
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun StatusChips( private fun StatusChips(status: EventStatus, accessLevel: AccessLevel) {
status: EventStatus,
availability: Availability,
accessLevel: AccessLevel,
) {
FlowRow( FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
@@ -425,13 +438,6 @@ private fun StatusChips(
EventStatus.Confirmed -> Unit EventStatus.Confirmed -> Unit
} }
val availabilityLabel = if (availability == Availability.Free) {
R.string.event_availability_free
} else {
R.string.event_availability_busy
}
InfoChip(text = stringResource(availabilityLabel))
when (accessLevel) { when (accessLevel) {
AccessLevel.Private -> InfoChip(text = stringResource(R.string.event_access_private)) AccessLevel.Private -> InfoChip(text = stringResource(R.string.event_access_private))
AccessLevel.Confidential -> AccessLevel.Confidential ->
@@ -444,10 +450,11 @@ private fun StatusChips(
@Composable @Composable
private fun InfoChip( private fun InfoChip(
text: String, text: String,
modifier: Modifier = Modifier,
container: Color = MaterialTheme.colorScheme.surfaceContainerHighest, container: Color = MaterialTheme.colorScheme.surfaceContainerHighest,
content: Color = MaterialTheme.colorScheme.onSurfaceVariant, content: Color = MaterialTheme.colorScheme.onSurfaceVariant,
) { ) {
Surface(color = container, shape = RoundedCornerShape(8.dp)) { Surface(color = container, shape = RoundedCornerShape(8.dp), modifier = modifier) {
Text( Text(
text = text, text = text,
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,

View File

@@ -72,7 +72,6 @@
<string name="event_status_tentative">Vorläufig</string> <string name="event_status_tentative">Vorläufig</string>
<string name="event_status_cancelled">Abgesagt</string> <string name="event_status_cancelled">Abgesagt</string>
<string name="event_availability_free">Frei</string> <string name="event_availability_free">Frei</string>
<string name="event_availability_busy">Gebucht</string>
<string name="event_access_private">Privat</string> <string name="event_access_private">Privat</string>
<string name="event_access_confidential">Vertraulich</string> <string name="event_access_confidential">Vertraulich</string>
<string name="event_attendee_organizer">Organisator</string> <string name="event_attendee_organizer">Organisator</string>

View File

@@ -73,7 +73,6 @@
<string name="event_status_tentative">Tentative</string> <string name="event_status_tentative">Tentative</string>
<string name="event_status_cancelled">Cancelled</string> <string name="event_status_cancelled">Cancelled</string>
<string name="event_availability_free">Free</string> <string name="event_availability_free">Free</string>
<string name="event_availability_busy">Busy</string>
<string name="event_access_private">Private</string> <string name="event_access_private">Private</string>
<string name="event_access_confidential">Confidential</string> <string name="event_access_confidential">Confidential</string>
<string name="event_attendee_organizer">Organizer</string> <string name="event_attendee_organizer">Organizer</string>