DRY pass #2, batch 6, F19: Sheet, the bottom sheet with its title (#5372)

EditorPickers.kt Sheet(title, onDismiss, modifier, verticalArrangement):
a ModalBottomSheet holding a full-width column with the screen margin,
clear of the navigation bar, under SheetTitle. The filter, tag picker,
reminder and share sheets each built that. A site's extra (a scroll, ime
padding, bottom space) is now applied after the navigation-bar inset
rather than before it. The total inset is the same either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:53:51 -04:00
co-authored by Claude Opus 5.5
parent f2fac1674b
commit 709ccca64c
3 changed files with 167 additions and 167 deletions
@@ -1,19 +1,16 @@
package com.fabledsword.inkwell.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -115,53 +112,46 @@ internal fun FilterSheet(
onChange: (BoardFilters) -> Unit,
onDismiss: () -> Unit,
) {
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.verticalScroll(rememberScrollState()),
) {
SheetTitle(R.string.filters)
Sheet(
title = R.string.filters,
onDismiss = onDismiss,
modifier = Modifier.verticalScroll(rememberScrollState()),
) {
FilterHeading(R.string.filters_show)
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(
selected = filters.hasAttachment,
onClick = { onChange(filters.copy(hasAttachment = !filters.hasAttachment)) },
label = { Text(stringResource(R.string.filters_has_attachment)) },
)
FilterChip(
selected = filters.sharedWithMe,
onClick = { onChange(filters.copy(sharedWithMe = !filters.sharedWithMe)) },
label = { Text(stringResource(R.string.filters_shared_with_me)) },
)
}
FilterHeading(R.string.filters_show)
// No heading and no empty row when there are no tags yet: a section that
// can only ever say "none" is noise in a sheet this small.
if (labels.isNotEmpty()) {
FilterHeading(R.string.filters_tags)
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
FilterChip(
selected = filters.hasAttachment,
onClick = { onChange(filters.copy(hasAttachment = !filters.hasAttachment)) },
label = { Text(stringResource(R.string.filters_has_attachment)) },
)
FilterChip(
selected = filters.sharedWithMe,
onClick = { onChange(filters.copy(sharedWithMe = !filters.sharedWithMe)) },
label = { Text(stringResource(R.string.filters_shared_with_me)) },
)
}
// No heading and no empty row when there are no tags yet: a section that
// can only ever say "none" is noise in a sheet this small.
if (labels.isNotEmpty()) {
FilterHeading(R.string.filters_tags)
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
labels.forEach { label ->
FilterChip(
selected = label.id in filters.labelIds,
onClick = { onChange(filters.toggleLabel(label.id)) },
label = { Text(label.name) },
)
}
labels.forEach { label ->
FilterChip(
selected = label.id in filters.labelIds,
onClick = { onChange(filters.toggleLabel(label.id)) },
label = { Text(label.name) },
)
}
}
}
if (filters.count > 0) {
TextButton(
onClick = { onChange(BoardFilters()) },
modifier = Modifier.align(Alignment.End),
) {
Text(stringResource(R.string.filters_clear))
}
if (filters.count > 0) {
TextButton(
onClick = { onChange(BoardFilters()) },
modifier = Modifier.align(Alignment.End),
) {
Text(stringResource(R.string.filters_clear))
}
}
}
@@ -1,8 +1,10 @@
package com.fabledsword.inkwell.ui
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -85,69 +87,62 @@ fun LabelSheet(
.map { it.id }
.toSet()
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.imePadding()
.navigationBarsPadding(),
) {
SheetTitle(R.string.label_picker_title)
Sheet(
title = R.string.label_picker_title,
onDismiss = onDismiss,
modifier = Modifier.imePadding(),
) {
PlainTextField(
value = typed,
onValueChange = { typed = it },
hint = R.string.label_new_hint,
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions =
KeyboardActions(onDone = {
onAction(EditorAction.CreateLabel(typed))
typed = ""
}),
)
PlainTextField(
value = typed,
onValueChange = { typed = it },
hint = R.string.label_new_hint,
singleLine = true,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions =
KeyboardActions(onDone = {
onAction(EditorAction.CreateLabel(typed))
typed = ""
}),
)
// Capped rather than unbounded: a sheet that grows past the screen
// makes its own scroll fight the sheet's drag gesture.
LazyColumn(modifier = Modifier.heightIn(max = LABEL_LIST_MAX_HEIGHT)) {
items(items = labels, key = { it.id }) { label ->
val fromTag = label.id in viaTag
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = fromTag || label.id in manual,
enabled = !fromTag,
onCheckedChange = { on ->
val next = if (on) manual + label.id else manual - label.id
onAction(EditorAction.SetLabels(next.toList()))
},
)
// Capped rather than unbounded: a sheet that grows past the screen
// makes its own scroll fight the sheet's drag gesture.
LazyColumn(modifier = Modifier.heightIn(max = LABEL_LIST_MAX_HEIGHT)) {
items(items = labels, key = { it.id }) { label ->
val fromTag = label.id in viaTag
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(
checked = fromTag || label.id in manual,
enabled = !fromTag,
onCheckedChange = { on ->
val next = if (on) manual + label.id else manual - label.id
onAction(EditorAction.SetLabels(next.toList()))
},
)
Text(
text = label.name,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 4.dp),
)
if (fromTag) {
Text(
text = label.name,
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.padding(start = 4.dp),
text = stringResource(R.string.label_from_tag),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 8.dp),
)
if (fromTag) {
Text(
text = stringResource(R.string.label_from_tag),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 8.dp),
)
}
}
}
}
}
if (labels.isEmpty()) {
Text(
text = stringResource(R.string.label_none_body),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 16.dp),
)
}
if (labels.isEmpty()) {
Text(
text = stringResource(R.string.label_none_body),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 16.dp),
)
}
}
}
@@ -170,60 +165,53 @@ fun ReminderSheet(
) {
var exact by remember { mutableStateOf(false) }
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.navigationBarsPadding(),
) {
SheetTitle(R.string.reminder_title)
reminderPresets().forEach { (labelRes, at) ->
Text(
text = "${stringResource(labelRes)} · ${formatInstant(rfc3339(at))}",
style = MaterialTheme.typography.bodyLarge,
modifier =
Modifier
.fillMaxWidth()
.clickable {
onAction(EditorAction.SetReminder(rfc3339(at)))
onDismiss()
}.padding(vertical = 12.dp),
)
}
Sheet(
title = R.string.reminder_title,
onDismiss = onDismiss,
) {
reminderPresets().forEach { (labelRes, at) ->
Text(
text = stringResource(R.string.reminder_pick),
text = "${stringResource(labelRes)} · ${formatInstant(rfc3339(at))}",
style = MaterialTheme.typography.bodyLarge,
modifier =
Modifier
.fillMaxWidth()
.clickable { exact = true }
.padding(vertical = 12.dp),
.clickable {
onAction(EditorAction.SetReminder(rfc3339(at)))
onDismiss()
}.padding(vertical = 12.dp),
)
}
// Repeat only appears once there IS a reminder — a recurrence rule on
// a note with no time to recur from is a setting that does nothing.
if (note.remindAt != null) {
RecurrenceChips(
current = note.recurrence,
onPick = { onAction(EditorAction.SetRecurrence(it)) },
)
Text(
text = stringResource(R.string.reminder_clear),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error,
modifier =
Modifier
.fillMaxWidth()
.clickable {
onAction(EditorAction.ClearReminder)
onDismiss()
}.padding(vertical = 12.dp),
)
}
Text(
text = stringResource(R.string.reminder_pick),
style = MaterialTheme.typography.bodyLarge,
modifier =
Modifier
.fillMaxWidth()
.clickable { exact = true }
.padding(vertical = 12.dp),
)
// Repeat only appears once there IS a reminder — a recurrence rule on
// a note with no time to recur from is a setting that does nothing.
if (note.remindAt != null) {
RecurrenceChips(
current = note.recurrence,
onPick = { onAction(EditorAction.SetRecurrence(it)) },
)
Text(
text = stringResource(R.string.reminder_clear),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.error,
modifier =
Modifier
.fillMaxWidth()
.clickable {
onAction(EditorAction.ClearReminder)
onDismiss()
}.padding(vertical = 12.dp),
)
}
}
@@ -337,6 +325,36 @@ private fun RecurrenceChips(
}
}
/**
* A bottom sheet with its title: full width, the screen's side margin, and clear of
* the navigation bar. [modifier] adds to that, after the inset (ime padding, a
* scroll, extra space at the bottom).
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun Sheet(
@StringRes title: Int,
onDismiss: () -> Unit,
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
content: @Composable ColumnScope.() -> Unit,
) {
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.navigationBarsPadding()
.then(modifier),
verticalArrangement = verticalArrangement,
) {
SheetTitle(title)
content()
}
}
}
@Composable
internal fun SheetTitle(labelRes: Int) {
Text(
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -18,7 +17,6 @@ import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -75,23 +73,17 @@ fun ShareSheet(
onUnshare: (shareId: String) -> Unit,
onDismiss: () -> Unit,
) {
ModalBottomSheet(onDismissRequest = onDismiss) {
Column(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.navigationBarsPadding(),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
SheetTitle(R.string.share_title)
when {
state.loading -> Muted(stringResource(R.string.share_loading))
state.needsServer -> Muted(stringResource(R.string.share_needs_server))
state.loadError != null -> ErrorText(state.loadError)
else -> ShareBody(state, onShare, onUnshare)
}
Sheet(
title = R.string.share_title,
onDismiss = onDismiss,
modifier = Modifier.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
when {
state.loading -> Muted(stringResource(R.string.share_loading))
state.needsServer -> Muted(stringResource(R.string.share_needs_server))
state.loadError != null -> ErrorText(state.loadError)
else -> ShareBody(state, onShare, onUnshare)
}
}
}