feat: allow editing synced lists, not just device-only ones

The edit pencil was gated to local lists even though rename/delete already
worked end to end for synced collections. Read-only shares keep the icon
but show it disabled, with a hint on tap explaining why.

Refs #29
This commit is contained in:
2026-10-05 18:49:08 +02:00
parent d81e99084e
commit a4fbbf43c6
@@ -52,6 +52,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExtendedFloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MediumTopAppBar
import androidx.compose.material3.Scaffold
@@ -139,9 +140,12 @@ fun TaskListScreen(
val list = content?.list
val listName = list?.name
val listId = (filter as? TaskFilter.OfList)?.listId
// Editing is offered for a device-only list. A collection that belongs to an
// account is the server's to rename or remove, not ours.
// Editing is offered for any writable list, device or synced — renaming and
// recolouring a synced collection is a server write the same sheet already
// handles. A read-only share keeps the icon but disables it: tapping it
// explains why rather than the button silently not being there.
var editingList by rememberSaveable { mutableStateOf(false) }
var readOnlyHint by rememberSaveable { mutableStateOf(false) }
val listWriteFailure by viewModel.listWriteFailure.collectAsStateWithLifecycle()
val listDeleted by viewModel.listDeleted.collectAsStateWithLifecycle()
// The list this screen is about is gone; there is nothing left to show.
@@ -152,6 +156,12 @@ fun TaskListScreen(
viewModel.clearListWriteFailure()
}
}
LaunchedEffect(readOnlyHint) {
if (readOnlyHint) {
delay(4_000)
readOnlyHint = false
}
}
// One add affordance, never two: a real list with the setting on gets a pinned
// bottom quick-add bar; everything else (incl. smart lists, which have no single
// target list) gets the floating "New task" button.
@@ -181,11 +191,20 @@ fun TaskListScreen(
IconButton(onClick = { showSort = true }, enabled = content != null) {
Icon(Icons.AutoMirrored.Rounded.Sort, contentDescription = stringResource(R.string.sort_title))
}
if (list != null && list.isLocal) {
IconButton(onClick = { editingList = true }) {
if (list != null) {
IconButton(
onClick = {
if (list.isReadOnly) readOnlyHint = true else editingList = true
},
) {
Icon(
Icons.Rounded.Edit,
contentDescription = stringResource(R.string.list_edit_title),
tint = if (list.isReadOnly) {
LocalContentColor.current.copy(alpha = 0.38f)
} else {
LocalContentColor.current
},
)
}
}
@@ -230,23 +249,28 @@ fun TaskListScreen(
.height(SnackChipHeight),
contentAlignment = Alignment.CenterStart,
) {
// One chip, one anchor: the undo receipt takes precedence, and a
// refused list write reports itself once the undo window is clear.
// One chip, one anchor, in priority order: the undo receipt first,
// then a refused list write once the undo window is clear, then the
// read-only edit hint — the quietest of the three, so it never
// outranks something the other two are actually reporting.
val failure = listWriteFailure
if (undo != null || failure == null) {
SnackChip(
visible = undo != null,
when {
undo != null -> SnackChip(
visible = true,
message = stringResource(
if (undo is UndoReceipt.Completed) R.string.task_completed else R.string.task_deleted,
),
actionLabel = stringResource(R.string.undo),
onAction = viewModel::undo,
)
} else {
SnackChip(
failure != null -> SnackChip(
visible = true,
message = stringResource(listWriteFailureMessage(failure)),
)
else -> SnackChip(
visible = readOnlyHint,
message = stringResource(R.string.list_read_only),
)
}
}
}