2 Commits

Author SHA1 Message Date
45cd5fefcb docs: lead with the plan at a glance
The direction document opens with reasoning, which is right for settling a
decision and wrong for reading it back later. Put the whole plan — five
commitments and the order of work — in one scannable block at the top, and
leave everything below as the argument for it.

Kept in the same file rather than split into a fifth planning document:
ROADMAP.md, PLAN.md and ARCHITECTURE.md already overlap, and two of them
are stale.
2026-08-02 17:54:51 +02:00
7940134381 docs: settle the storage and sync direction
Replaces the "Posture B = bundle OpenTasks under org.dmfs.tasks" working
notes (never committed) with the decided direction: Agendula gets its own
identity all the way down — its own bundled provider under its own
authority, its own sync adapter, and a user-chosen storage mode.

Squatting org.dmfs.tasks is out. The decisive reason is structural rather
than aesthetic: content-provider authorities are what a sync engine
resolves, but Android account visibility is keyed by package name, so a
bundled provider masquerading as OpenTasks could see zero accounts — and
the dmfs provider prunes task lists whose account has gone away. That is a
silent data-loss bug invisible to both sides. Owning the namespace makes it
impossible by construction. Play adds a second veto: a duplicate authority
or permission name is a failed install for anyone with OpenTasks.

Also records: vendoring the Apache-2.0 provider in-tree (JitPack and Maven
Central both verified dead ends), the three storage modes, staged
permissions split by runtime vs install-time, what belongs in floret-kit
rather than here, and the F-Droid + Play constraints to build for from day
one.

ARCHITECTURE.md §7 and the ProviderResolver KDoc still describe the old
Posture B and need a follow-up edit; ROADMAP.md and PLAN.md remain stale
pending their own pass.
2026-08-02 17:54:51 +02:00
4 changed files with 53 additions and 24 deletions

View File

@@ -501,17 +501,13 @@ private fun taskWhenLines(task: Task): Pair<String, String?>? {
val due = task.due val due = task.due
return when { return when {
start != null && due != null -> { start != null && due != null -> {
val allDay = task.isAllDay val sameDay = start.formatDate() == due.formatDate()
val sameDay = start.formatDate(allDay) == due.formatDate(allDay) val primary = if (sameDay) due.formatDate() else "${start.formatDate()} ${due.formatDate()}"
val primary = val secondary = if (task.isAllDay) null else "${start.formatTime()} ${due.formatTime()}"
if (sameDay) due.formatDate(allDay)
else "${start.formatDate(allDay)} ${due.formatDate(allDay)}"
val secondary = if (allDay) null else "${start.formatTime()} ${due.formatTime()}"
primary to secondary primary to secondary
} }
due != null -> due.formatDate(task.isAllDay) to if (task.isAllDay) null else due.formatTime() due != null -> due.formatDate() to if (task.isAllDay) null else due.formatTime()
start != null -> start != null -> start.formatDate() to if (task.isAllDay) null else start.formatTime()
start.formatDate(task.isAllDay) to if (task.isAllDay) null else start.formatTime()
else -> null else -> null
} }
} }

View File

@@ -653,7 +653,7 @@ private fun ScheduleRow(
) )
} else { } else {
Text( Text(
text = value.formatDate(allDay), text = value.formatDate(),
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = valueColor, color = valueColor,
modifier = Modifier.clickable(onClick = onPick).padding(vertical = 8.dp, horizontal = 6.dp), modifier = Modifier.clickable(onClick = onPick).padding(vertical = 8.dp, horizontal = 6.dp),
@@ -865,7 +865,7 @@ private fun ParentPickerSheet(
GroupedRow( GroupedRow(
title = task.title.ifBlank { stringResource(R.string.task_untitled) }, title = task.title.ifBlank { stringResource(R.string.task_untitled) },
position = positionOf(index, section.tasks.size), position = positionOf(index, section.tasks.size),
summary = task.due?.formatDate(task.isAllDay), summary = task.due?.formatDate(),
selected = task.taskId == selectedId, selected = task.taskId == selectedId,
minHeight = 56.dp, minHeight = 56.dp,
onClick = { choose(task.taskId) }, onClick = { choose(task.taskId) },

View File

@@ -98,9 +98,6 @@ import de.jeanlucmakiola.agendula.domain.TaskSection
import de.jeanlucmakiola.agendula.domain.TaskSections import de.jeanlucmakiola.agendula.domain.TaskSections
import de.jeanlucmakiola.agendula.ui.common.priorityAccent import de.jeanlucmakiola.agendula.ui.common.priorityAccent
import de.jeanlucmakiola.floret.components.Position import de.jeanlucmakiola.floret.components.Position
import de.jeanlucmakiola.floret.components.SnackChip
import de.jeanlucmakiola.floret.components.SnackChipHeight
import de.jeanlucmakiola.floret.components.SnackChipMargin
import de.jeanlucmakiola.floret.time.formatDateTimeCompact import de.jeanlucmakiola.floret.time.formatDateTimeCompact
import de.jeanlucmakiola.floret.components.pastelize import de.jeanlucmakiola.floret.components.pastelize
import de.jeanlucmakiola.floret.components.positionOf import de.jeanlucmakiola.floret.components.positionOf
@@ -192,18 +189,13 @@ fun TaskListScreen(
Box( Box(
modifier = Modifier modifier = Modifier
.align(Alignment.BottomStart) .align(Alignment.BottomStart)
.padding( .padding(start = 16.dp, bottom = inner.calculateBottomPadding() + 16.dp)
start = SnackChipMargin, .height(56.dp),
bottom = inner.calculateBottomPadding() + SnackChipMargin,
)
.height(SnackChipHeight),
contentAlignment = Alignment.CenterStart, contentAlignment = Alignment.CenterStart,
) { ) {
SnackChip( UndoChip(
visible = undoTarget != null, visible = undoTarget != null,
message = stringResource(R.string.task_deleted), onUndo = {
actionLabel = stringResource(R.string.undo),
onAction = {
undoTarget?.let { viewModel.undoDelete(it.taskId) } undoTarget?.let { viewModel.undoDelete(it.taskId) }
undoTarget = null undoTarget = null
}, },
@@ -213,6 +205,47 @@ fun TaskListScreen(
} }
} }
/**
* A compact floating "snackchip" for an undoable delete — a rounded pill (not a
* full-width snackbar) sized to its content, sliding up from the bottom centre.
*/
@Composable
private fun UndoChip(visible: Boolean, onUndo: () -> Unit, modifier: Modifier = Modifier) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically { it } + fadeIn(),
exit = slideOutVertically { it } + fadeOut(),
modifier = modifier,
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHighest,
contentColor = MaterialTheme.colorScheme.onSurface,
shape = RoundedCornerShape(50),
shadowElevation = 6.dp,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 20.dp, end = 8.dp, top = 6.dp, bottom = 6.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = stringResource(R.string.task_deleted),
style = MaterialTheme.typography.bodyMedium,
)
TextButton(
onClick = onUndo,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp),
) {
Text(
text = stringResource(R.string.undo),
style = MaterialTheme.typography.labelLarge,
)
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable @Composable
private fun TaskListBody( private fun TaskListBody(