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:
@@ -1,19 +1,16 @@
|
|||||||
package com.fabledsword.inkwell.ui
|
package com.fabledsword.inkwell.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -115,53 +112,46 @@ internal fun FilterSheet(
|
|||||||
onChange: (BoardFilters) -> Unit,
|
onChange: (BoardFilters) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
Sheet(
|
||||||
Column(
|
title = R.string.filters,
|
||||||
modifier =
|
onDismiss = onDismiss,
|
||||||
Modifier
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
.fillMaxWidth()
|
) {
|
||||||
.padding(horizontal = 16.dp)
|
FilterHeading(R.string.filters_show)
|
||||||
.navigationBarsPadding()
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
.verticalScroll(rememberScrollState()),
|
FilterChip(
|
||||||
) {
|
selected = filters.hasAttachment,
|
||||||
SheetTitle(R.string.filters)
|
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)) {
|
FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||||
FilterChip(
|
labels.forEach { label ->
|
||||||
selected = filters.hasAttachment,
|
FilterChip(
|
||||||
onClick = { onChange(filters.copy(hasAttachment = !filters.hasAttachment)) },
|
selected = label.id in filters.labelIds,
|
||||||
label = { Text(stringResource(R.string.filters_has_attachment)) },
|
onClick = { onChange(filters.toggleLabel(label.id)) },
|
||||||
)
|
label = { Text(label.name) },
|
||||||
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) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (filters.count > 0) {
|
if (filters.count > 0) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = { onChange(BoardFilters()) },
|
onClick = { onChange(BoardFilters()) },
|
||||||
modifier = Modifier.align(Alignment.End),
|
modifier = Modifier.align(Alignment.End),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.filters_clear))
|
Text(stringResource(R.string.filters_clear))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.fabledsword.inkwell.ui
|
package com.fabledsword.inkwell.ui
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
@@ -85,69 +87,62 @@ fun LabelSheet(
|
|||||||
.map { it.id }
|
.map { it.id }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
Sheet(
|
||||||
Column(
|
title = R.string.label_picker_title,
|
||||||
modifier =
|
onDismiss = onDismiss,
|
||||||
Modifier
|
modifier = Modifier.imePadding(),
|
||||||
.fillMaxWidth()
|
) {
|
||||||
.padding(horizontal = 16.dp)
|
PlainTextField(
|
||||||
.imePadding()
|
value = typed,
|
||||||
.navigationBarsPadding(),
|
onValueChange = { typed = it },
|
||||||
) {
|
hint = R.string.label_new_hint,
|
||||||
SheetTitle(R.string.label_picker_title)
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(onDone = {
|
||||||
|
onAction(EditorAction.CreateLabel(typed))
|
||||||
|
typed = ""
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
PlainTextField(
|
// Capped rather than unbounded: a sheet that grows past the screen
|
||||||
value = typed,
|
// makes its own scroll fight the sheet's drag gesture.
|
||||||
onValueChange = { typed = it },
|
LazyColumn(modifier = Modifier.heightIn(max = LABEL_LIST_MAX_HEIGHT)) {
|
||||||
hint = R.string.label_new_hint,
|
items(items = labels, key = { it.id }) { label ->
|
||||||
singleLine = true,
|
val fromTag = label.id in viaTag
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
keyboardActions =
|
Checkbox(
|
||||||
KeyboardActions(onDone = {
|
checked = fromTag || label.id in manual,
|
||||||
onAction(EditorAction.CreateLabel(typed))
|
enabled = !fromTag,
|
||||||
typed = ""
|
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.
|
Text(
|
||||||
LazyColumn(modifier = Modifier.heightIn(max = LABEL_LIST_MAX_HEIGHT)) {
|
text = label.name,
|
||||||
items(items = labels, key = { it.id }) { label ->
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
val fromTag = label.id in viaTag
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
)
|
||||||
Checkbox(
|
if (fromTag) {
|
||||||
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(
|
||||||
text = label.name,
|
text = stringResource(R.string.label_from_tag),
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
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()) {
|
if (labels.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.label_none_body),
|
text = stringResource(R.string.label_none_body),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,60 +165,53 @@ fun ReminderSheet(
|
|||||||
) {
|
) {
|
||||||
var exact by remember { mutableStateOf(false) }
|
var exact by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
Sheet(
|
||||||
Column(
|
title = R.string.reminder_title,
|
||||||
modifier =
|
onDismiss = onDismiss,
|
||||||
Modifier
|
) {
|
||||||
.fillMaxWidth()
|
reminderPresets().forEach { (labelRes, at) ->
|
||||||
.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),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.reminder_pick),
|
text = "${stringResource(labelRes)} · ${formatInstant(rfc3339(at))}",
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { exact = true }
|
.clickable {
|
||||||
.padding(vertical = 12.dp),
|
onAction(EditorAction.SetReminder(rfc3339(at)))
|
||||||
|
onDismiss()
|
||||||
|
}.padding(vertical = 12.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Repeat only appears once there IS a reminder — a recurrence rule on
|
Text(
|
||||||
// a note with no time to recur from is a setting that does nothing.
|
text = stringResource(R.string.reminder_pick),
|
||||||
if (note.remindAt != null) {
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
RecurrenceChips(
|
modifier =
|
||||||
current = note.recurrence,
|
Modifier
|
||||||
onPick = { onAction(EditorAction.SetRecurrence(it)) },
|
.fillMaxWidth()
|
||||||
)
|
.clickable { exact = true }
|
||||||
Text(
|
.padding(vertical = 12.dp),
|
||||||
text = stringResource(R.string.reminder_clear),
|
)
|
||||||
style = MaterialTheme.typography.bodyLarge,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
// Repeat only appears once there IS a reminder — a recurrence rule on
|
||||||
modifier =
|
// a note with no time to recur from is a setting that does nothing.
|
||||||
Modifier
|
if (note.remindAt != null) {
|
||||||
.fillMaxWidth()
|
RecurrenceChips(
|
||||||
.clickable {
|
current = note.recurrence,
|
||||||
onAction(EditorAction.ClearReminder)
|
onPick = { onAction(EditorAction.SetRecurrence(it)) },
|
||||||
onDismiss()
|
)
|
||||||
}.padding(vertical = 12.dp),
|
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
|
@Composable
|
||||||
internal fun SheetTitle(labelRes: Int) {
|
internal fun SheetTitle(labelRes: Int) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -18,7 +17,6 @@ import androidx.compose.material3.FilterChip
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.ModalBottomSheet
|
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@@ -75,23 +73,17 @@ fun ShareSheet(
|
|||||||
onUnshare: (shareId: String) -> Unit,
|
onUnshare: (shareId: String) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ModalBottomSheet(onDismissRequest = onDismiss) {
|
Sheet(
|
||||||
Column(
|
title = R.string.share_title,
|
||||||
modifier =
|
onDismiss = onDismiss,
|
||||||
Modifier
|
modifier = Modifier.padding(bottom = 16.dp),
|
||||||
.fillMaxWidth()
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
.padding(horizontal = 16.dp)
|
) {
|
||||||
.padding(bottom = 16.dp)
|
when {
|
||||||
.navigationBarsPadding(),
|
state.loading -> Muted(stringResource(R.string.share_loading))
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
state.needsServer -> Muted(stringResource(R.string.share_needs_server))
|
||||||
) {
|
state.loadError != null -> ErrorText(state.loadError)
|
||||||
SheetTitle(R.string.share_title)
|
else -> ShareBody(state, onShare, onUnshare)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user