Add tick boxes to task rows
Tasks now carry a tick box rather than being read-only rows. Ticking strikes the title through and leaves the item in place, so a day still reads as a record of what was asked rather than only what is left. New in designs/Checkbox.dart: - TickBox the square box itself, animating between empty and filled - TickRow box + struck-through title + caller-supplied detail and trailing - addTaskAffordance the centred "Add new task" control beneath a day's list Applied to the home plan, the overdue queue and the goal task list. Each screen holds a set of optimistically ticked ids so the tick lands immediately and rolls back if the call fails; the set is reconciled whenever fresh data arrives. A tick completes the item outright, whatever its proof requirement. The proof flow is still reachable from the Complete button in the overdue queue, so tasks that need an artefact have a route that asks for one — but the tick itself no longer enforces it. Late completes stay visually distinct: the row keeps a Late pill and the warning accent, and the user is told the window had already closed. flutter analyze lib/ reports no errors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mfu2gQLSFN21YRBcU2NrTt
This commit is contained in:
@@ -4,11 +4,12 @@ import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../about/external/data/Commitment.dart';
|
||||
import '../../about/external/data/Goal.dart';
|
||||
import '../../about/internal/application/CommitmentClass.dart';
|
||||
import '../../about/internal/application/CommitmentStatus.dart';
|
||||
import '../../about/internal/application/NotificationType.dart';
|
||||
import '../../about/internal/application/ProofType.dart';
|
||||
import '../../about/internal/application/TextType.dart';
|
||||
import '../../configs/Navigator.dart';
|
||||
import '../../designs/Checkbox.dart';
|
||||
import '../../designs/Component.dart';
|
||||
import '../../designs/Responsive.dart';
|
||||
import '../../designs/Shell.dart';
|
||||
@@ -33,6 +34,9 @@ class GoalDetailState extends State<GoalDetail>
|
||||
|
||||
bool _changed = false;
|
||||
|
||||
/// Ticked locally, awaiting the server.
|
||||
final Set<String> _ticked = <String>{};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewGoalDetail>.reactive(
|
||||
@@ -70,6 +74,20 @@ class GoalDetailState extends State<GoalDetail>
|
||||
_model?.startTask(task);
|
||||
}
|
||||
|
||||
/// Ticking completes the task without opening the runner — the runner is for
|
||||
/// work you actually want timed.
|
||||
void _onTick(Commitment task) {
|
||||
if (task.id == null || _ticked.contains(task.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_ticked.add(task.id!);
|
||||
});
|
||||
|
||||
_model?.tick(task);
|
||||
}
|
||||
|
||||
void _onAddTask() async {
|
||||
final result = await GroundedNavigation()
|
||||
.navigateToPageWithData(const NewCommitment(), context);
|
||||
@@ -180,51 +198,29 @@ class GoalDetailState extends State<GoalDetail>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 3,
|
||||
height: 38,
|
||||
margin: const EdgeInsets.only(right: 14, top: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
TickRow(
|
||||
checked: _ticked.contains(task.id),
|
||||
title: task.title,
|
||||
accent: accent,
|
||||
onTick: () => _onTick(task),
|
||||
detail: Row(
|
||||
children: [
|
||||
text(formatWindow(task), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
const SizedBox(width: 9),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorGrey, shape: BoxShape.circle),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
text(task.title, 16, TextType.Medium,
|
||||
color: colorPrimaryDark,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
const SizedBox(height: 7),
|
||||
Row(
|
||||
children: [
|
||||
text(formatWindow(task), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
const SizedBox(width: 9),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: colorGrey, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 9),
|
||||
text(formatMinutes(task.estMinutes), 11,
|
||||
TextType.Regular, color: colorGrey2),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
pill(proofLabel(task.proofType), colorGrey2, colorMuted,
|
||||
textSize: 9),
|
||||
],
|
||||
const SizedBox(width: 9),
|
||||
text(formatMinutes(task.estMinutes), 11, TextType.Regular,
|
||||
color: colorGrey2),
|
||||
],
|
||||
),
|
||||
trailing: pill(proofLabel(task.proofType), colorGrey2, colorMuted,
|
||||
textSize: 9),
|
||||
),
|
||||
if (late) ...[
|
||||
const SizedBox(height: 12),
|
||||
@@ -258,26 +254,15 @@ class GoalDetailState extends State<GoalDetail>
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: card(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
late
|
||||
? CupertinoIcons.checkmark_circle
|
||||
: CupertinoIcons.checkmark_circle_fill,
|
||||
size: 17,
|
||||
color: late ? colorStandingWarned : colorPositive,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: text(task.title, 13, TextType.Regular,
|
||||
color: colorGrey2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis),
|
||||
),
|
||||
if (late)
|
||||
pill("Late", colorStandingWarned, colorStandingWarnedBg,
|
||||
textSize: 9),
|
||||
],
|
||||
child: TickRow(
|
||||
checked: true,
|
||||
locked: true,
|
||||
title: task.title,
|
||||
accent: late ? colorStandingWarned : colorPositive,
|
||||
trailing: late
|
||||
? pill("Late", colorStandingWarned, colorStandingWarnedBg,
|
||||
textSize: 9)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -288,11 +273,38 @@ class GoalDetailState extends State<GoalDetail>
|
||||
@override
|
||||
void onGoalLoaded(Goal goal, List<Commitment> tasks) {
|
||||
setState(() {
|
||||
_ticked.removeWhere((id) => tasks.every((task) => task.id != id));
|
||||
_goal = goal;
|
||||
_tasks = tasks;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onTaskTicked(Commitment task, bool late) {
|
||||
_changed = true;
|
||||
_model?.loadTasks(_goal);
|
||||
|
||||
if (!late) {
|
||||
return;
|
||||
}
|
||||
|
||||
_model?.showApplicationNotification(
|
||||
NotificationType.warning,
|
||||
"Late complete",
|
||||
"The window had already closed. This is recorded as a late complete — it reduces the debt but does not clear it.",
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onTickFailed(Commitment task) {
|
||||
setState(() {
|
||||
_ticked.remove(task.id);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void onTaskReady(Commitment task) async {
|
||||
// The runner takes over the whole screen — a task you are running is the
|
||||
|
||||
Reference in New Issue
Block a user