feat(todo): quick add's keys, sub-projects' colours, a task's menu on a right click, and the completed tasks deleted at once

The developer's list of 9 Nov 2026, Todo's part:

- Quick add: the down arrow and Tab move into the description, with its
  cursor (halkyon_editor's fix); Escape closes the projects or labels
  suggested first, then closes quick add, what was typed discarded; the
  project's chip shows the project's colour.
- A project without a colour of its own shows its parent's, the nearest
  ancestor's with one (colorOfProject, TodoCubit.colorOf), wherever a
  project's colour shows; the project editor's "Same colour as its parent"
  keeps it so, and the parent picker shows each project in its colour. A
  project of its own always has one.
- The task pane's x closes it, always; Close left the ⋮ menu.
- A task's row opens its menu on a right click: Mark as done, Date, Move
  to, Labels, Priority, Deadline, Duplicate and Delete, the selection's
  actions for that one task.
- Delete completed tasks, in the ⋮ of the Inbox, Today, Upcoming and a
  project, with Undo: the tasks leave the list at once and are deleted
  once the snack bar goes without its Undo (halkyon_ui's showUndoSnackBar
  gains onKept).

Whats-New: Quick add: the arrow keys and Tab reach the description, and Escape closes it without saving.
Whats-New-De: Schnell hinzufügen: Pfeiltasten und Tab erreichen die Beschreibung, und Escape schließt es, ohne zu speichern.
Whats-New: Sub-projects take their parent's colour unless they have their own.
Whats-New-De: Unterprojekte übernehmen die Farbe ihres übergeordneten Projekts, wenn sie keine eigene haben.
Whats-New: A right click on a task opens its menu, with Move to and more; Delete completed tasks clears a list, with Undo.
Whats-New-De: Ein Rechtsklick auf eine Aufgabe öffnet ihr Menü, mit Verschieben nach und mehr; Erledigte Aufgaben löschen räumt eine Liste auf, mit Rückgängig.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-09 23:12:43 +01:00
commit 9e1442d89e
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
26 changed files with 810 additions and 78 deletions

View file

@ -132,6 +132,7 @@
"projectDelete": "Projekt löschen",
"projectDeleteTitle": "Projekt löschen?",
"projectDeleteMessage": "„{name}“ wird mit seinen Abschnitten und Aufgaben auf all deinen Geräten gelöscht.",
"projectInheritColor": "Gleiche Farbe wie das übergeordnete Projekt",
"projectParent": "Übergeordnetes Projekt",
"projectNoParent": "Kein übergeordnetes Projekt",
"projectFavorite": "Favorit",
@ -231,7 +232,6 @@
"taskDuplicated": "Aufgabe dupliziert",
"subTasksHideDone": "Erledigte Unteraufgaben ausblenden",
"subTasksShowDone": "Erledigte Unteraufgaben zeigen",
"taskClose": "Schließen",
"taskPath": "Wo die Aufgabe liegt",
"weekToday": "Zurück zu heute",
"monthPrevious": "Voriger Monat",
@ -402,5 +402,7 @@
"locationOffTitle": "Standort ist für Erinnerungen aus",
"locationOffHint": "Erinnerungen an einem Ort brauchen „Immer zulassen“.",
"locationArrived": "Du bist bei {name}",
"locationLeft": "Du hast {name} verlassen"
"locationLeft": "Du hast {name} verlassen",
"tasksDeleteCompleted": "Erledigte Aufgaben löschen",
"tasksCompletedDeleted": "{count, plural, =1{1 erledigte Aufgabe gelöscht} other{{count} erledigte Aufgaben gelöscht}}"
}

View file

@ -282,6 +282,10 @@
}
}
},
"projectInheritColor": "Same colour as its parent",
"@projectInheritColor": {
"description": "In a sub-project's editor: whether it shows in its parent's colour rather than one of its own (the developer's review, 9 Nov 2026)."
},
"projectParent": "Parent project",
"@projectParent": {
"description": "A project's editor (ADR 0033, D28), as Todoist's Add project."
@ -598,7 +602,6 @@
"taskDuplicated": "Task duplicated",
"subTasksHideDone": "Hide done sub-tasks",
"subTasksShowDone": "Show done sub-tasks",
"taskClose": "Close",
"taskPath": "Where the task is",
"weekToday": "Back to today",
"monthPrevious": "Previous month",
@ -1246,5 +1249,18 @@
"type": "String"
}
}
},
"tasksDeleteCompleted": "Delete completed tasks",
"@tasksDeleteCompleted": {
"description": "A list's menu: deletes its done tasks, with an Undo (the developer's review, 9 Nov 2026)."
},
"tasksCompletedDeleted": "{count, plural, =1{1 completed task deleted} other{{count} completed tasks deleted}}",
"@tasksCompletedDeleted": {
"description": "The snack bar after Delete completed tasks, with its Undo.",
"placeholders": {
"count": {
"type": "int"
}
}
}
}

View file

@ -1,5 +1,6 @@
{
"@@locale": "en_US",
"projectFavorite": "Favorite",
"favoritesTitle": "Favorites"
"favoritesTitle": "Favorites",
"projectInheritColor": "Same color as its parent"
}

View file

@ -892,6 +892,12 @@ abstract class AppLocalizations {
/// **'\"{name}\" is deleted with its sections and tasks, on all your devices.'**
String projectDeleteMessage(String name);
/// In a sub-project's editor: whether it shows in its parent's colour rather than one of its own (the developer's review, 9 Nov 2026).
///
/// In en, this message translates to:
/// **'Same colour as its parent'**
String get projectInheritColor;
/// A project's editor (ADR 0033, D28), as Todoist's Add project.
///
/// In en, this message translates to:
@ -1486,12 +1492,6 @@ abstract class AppLocalizations {
/// **'Show done sub-tasks'**
String get subTasksShowDone;
/// No description provided for @taskClose.
///
/// In en, this message translates to:
/// **'Close'**
String get taskClose;
/// No description provided for @taskPath.
///
/// In en, this message translates to:
@ -2517,6 +2517,18 @@ abstract class AppLocalizations {
/// In en, this message translates to:
/// **'You\'ve left {name}'**
String locationLeft(String name);
/// A list's menu: deletes its done tasks, with an Undo (the developer's review, 9 Nov 2026).
///
/// In en, this message translates to:
/// **'Delete completed tasks'**
String get tasksDeleteCompleted;
/// The snack bar after Delete completed tasks, with its Undo.
///
/// In en, this message translates to:
/// **'{count, plural, =1{1 completed task deleted} other{{count} completed tasks deleted}}'**
String tasksCompletedDeleted(int count);
}
class _AppLocalizationsDelegate

View file

@ -487,6 +487,10 @@ class AppLocalizationsDe extends AppLocalizations {
return '„$name“ wird mit seinen Abschnitten und Aufgaben auf all deinen Geräten gelöscht.';
}
@override
String get projectInheritColor =>
'Gleiche Farbe wie das übergeordnete Projekt';
@override
String get projectParent => 'Übergeordnetes Projekt';
@ -921,9 +925,6 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get subTasksShowDone => 'Erledigte Unteraufgaben zeigen';
@override
String get taskClose => 'Schließen';
@override
String get taskPath => 'Wo die Aufgabe liegt';
@ -1574,4 +1575,18 @@ class AppLocalizationsDe extends AppLocalizations {
String locationLeft(String name) {
return 'Du hast $name verlassen';
}
@override
String get tasksDeleteCompleted => 'Erledigte Aufgaben löschen';
@override
String tasksCompletedDeleted(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count erledigte Aufgaben gelöscht',
one: '1 erledigte Aufgabe gelöscht',
);
return '$_temp0';
}
}

View file

@ -487,6 +487,9 @@ class AppLocalizationsEn extends AppLocalizations {
return '\"$name\" is deleted with its sections and tasks, on all your devices.';
}
@override
String get projectInheritColor => 'Same colour as its parent';
@override
String get projectParent => 'Parent project';
@ -921,9 +924,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get subTasksShowDone => 'Show done sub-tasks';
@override
String get taskClose => 'Close';
@override
String get taskPath => 'Where the task is';
@ -1572,6 +1572,20 @@ class AppLocalizationsEn extends AppLocalizations {
String locationLeft(String name) {
return 'You\'ve left $name';
}
@override
String get tasksDeleteCompleted => 'Delete completed tasks';
@override
String tasksCompletedDeleted(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '$count completed tasks deleted',
one: '1 completed task deleted',
);
return '$_temp0';
}
}
/// The translations for English, as used in the United Kingdom (`en_GB`).
@ -1583,6 +1597,9 @@ class AppLocalizationsEnGb extends AppLocalizationsEn {
class AppLocalizationsEnUs extends AppLocalizationsEn {
AppLocalizationsEnUs() : super('en_US');
@override
String get projectInheritColor => 'Same color as its parent';
@override
String get projectFavorite => 'Favorite';

View file

@ -1,4 +1,5 @@
import 'package:halkyon_todo_flutter/src/feature/project/model/stored_project.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
/// A project as Browse lists it (ADR 0033, D28): how deep it stands under
/// the projects it is a sub-project of, and whether it has sub-projects.
@ -83,3 +84,24 @@ List<StoredProject> parentChoices(List<StoredProject> projects, String? id) {
if (!excluded.contains(node.project.id)) node.project,
];
}
/// The colour the project [id] shows in: its own, or else its nearest
/// ancestor's (a sub-project inherits it, as Learn's sub-studysets do; the
/// developer's review, 9 Nov 2026); none, the theme's, where no project up
/// to one of its own has one, or the parents run in a circle.
PaletteColor? colorOfProject(List<StoredProject> projects, String id) {
final byId = {for (final project in projects) project.id: project};
final seen = <String>{};
String? at = id;
while (at != null && seen.add(at)) {
final content = byId[at]?.content;
if (content == null) {
return null;
}
if (content.color case final color?) {
return color;
}
at = content.parentId;
}
return null;
}

View file

@ -58,7 +58,11 @@ class const _ProjectEditor({
class _ProjectEditorState() extends State<_ProjectEditor> {
late final _name = TextEditingController(text: widget.current?.name);
late var _description = widget.current?.description;
late var _color = widget.current?.color ?? PaletteColor.cobalt;
/// Its own colour; none: its parent's, while it has one (a new project
/// takes it at first, as Learn's sub-studysets do; the developer's review,
/// 9 Nov 2026).
late PaletteColor? _color = widget.current?.color;
late var _parentId = widget.current?.parentId;
late var _favorite = widget.current?.favorite ?? false;
@ -92,7 +96,7 @@ class _ProjectEditorState() extends State<_ProjectEditor> {
// A new project starts with its layout (D28).
null => ProjectContent(
name: name,
color: _color,
color: _savedColor,
description: description,
parentId: _parentId,
favorite: _favorite,
@ -103,7 +107,7 @@ class _ProjectEditorState() extends State<_ProjectEditor> {
// 4 Nov 2026); a layout picked anew shows at once.
final current => current.copyWith(
name: name,
color: () => _color,
color: () => _savedColor,
description: description,
parentId: () => _parentId,
favorite: _favorite,
@ -115,6 +119,21 @@ class _ProjectEditorState() extends State<_ProjectEditor> {
});
}
/// Whether it shows in its parent's colour.
bool get _inheriting => _parentId != null && _color == null;
/// The colour it shows in: its own, else its parent's, else cobalt.
PaletteColor get _shownColor =>
_color ??
switch (_parentId) {
final parent? => colorOfProject(widget.projects, parent),
null => null,
} ??
PaletteColor.cobalt;
/// What it is saved with: a project of its own always has a colour.
PaletteColor? get _savedColor => _parentId == null ? _shownColor : _color;
StoredProject? get _parent => switch (_parentId) {
final id? => widget.projects.where((p) => p.id == id).firstOrNull,
null => null,
@ -129,7 +148,7 @@ class _ProjectEditorState() extends State<_ProjectEditor> {
content: SizedBox(
width: 320,
child: PaletteColorPicker(
selected: _color,
selected: _shownColor,
onChanged: (color) => Navigator.of(context).pop(color),
),
),
@ -148,7 +167,13 @@ class _ProjectEditorState() extends State<_ProjectEditor> {
current: _parentId,
);
if (picked case (:final parentId)) {
setState(() => _parentId = parentId);
setState(() {
// Out from under a parent, it keeps the colour it showed.
if (parentId == null) {
_color = _shownColor;
}
_parentId = parentId;
});
}
}
@ -205,15 +230,26 @@ class _ProjectEditorState() extends State<_ProjectEditor> {
ListTile(
leading: Icon(
LucideIcons.palette,
color: paletteText(context, _color),
color: paletteText(context, _shownColor),
),
title: Text(l10n.colorLabel),
trailing: Text(
ui.paletteColorName(_color.name),
ui.paletteColorName(_shownColor.name),
style: TextStyle(color: muted),
),
onTap: _pickColor,
),
if (_parentId != null)
CheckboxListTile(
// Lined up with the rows' icons.
controlAffinity: ListTileControlAffinity.leading,
title: Text(l10n.projectInheritColor),
value: _inheriting,
// Unticked, it keeps the colour it was showing.
onChanged: (inherit) => setState(
() => _color = inherit ?? false ? null : _shownColor,
),
),
ListTile(
leading: const Icon(LucideIcons.folderTree),
title: Text(l10n.projectParent),
@ -278,6 +314,13 @@ Future<({String? parentId})?> pickParentProject(
contentPadding: EdgeInsetsDirectional.only(start: 16 + 24.0 * depth),
leading: Icon(
parentId == null ? LucideIcons.circleSlash : LucideIcons.hash,
color: switch (parentId) {
final id? => switch (colorOfProject(projects, id)) {
final color? => paletteText(context, color),
null => null,
},
null => null,
},
),
title: Text(name),
trailing: parentId == current ? const Icon(LucideIcons.check) : null,

View file

@ -9,6 +9,7 @@ import 'package:halkyon_todo_flutter/src/feature/project/widget/display_sheet.da
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_editor.dart';
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_tasks.dart';
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_drag.dart';
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_refresh.dart';
import 'package:halkyon_todo_flutter/src/tabs/tab_screen.dart';
@ -43,6 +44,13 @@ class const ProjectScreenBody({
child: MenuEntry(LucideIcons.listPlus, l10n.sectionAdd),
),
selectTasksEntry(context),
deleteCompletedEntry(
context,
completedIn(
context.read<TodoCubit>().state.tasks,
projectId: projectId,
),
),
...?menu?.call(context),
],
),
@ -160,7 +168,7 @@ class const ProjectScreen({required final String projectId, super.key})
),
],
);
return switch (content.color) {
return switch (state.colorOf(projectId)) {
final color? => ScreenColor(color: color.background, child: body),
null => body,
};

View file

@ -177,7 +177,7 @@ class _ReportingScreenState() extends State<ReportingScreen> {
Icon(
project.inbox ? LucideIcons.inbox : LucideIcons.hash,
size: 18,
color: switch (content.color) {
color: switch (state.colorOf(project.id)) {
final color? when !project.inbox => paletteText(
context,
color,

View file

@ -8,10 +8,12 @@ import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
import 'package:halkyon_todo_flutter/src/feature/comment/model/comment.dart';
import 'package:halkyon_todo_flutter/src/feature/filter/model/saved_filter.dart';
import 'package:halkyon_todo_flutter/src/feature/label/model/label.dart';
import 'package:halkyon_todo_flutter/src/feature/project/model/project_tree.dart';
import 'package:halkyon_todo_flutter/src/feature/project/model/stored_project.dart';
import 'package:halkyon_todo_flutter/src/feature/section/model/section.dart';
import 'package:halkyon_todo_flutter/src/feature/settings/model/todo_settings.dart';
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
import 'package:halkyon_ui/halkyon_ui.dart';
/// Everything of the person's that Todo's screens show, as it stands on
/// this device (ADR 0033, D8): their projects (the Inbox first), sections,
@ -55,6 +57,10 @@ final class const TodoState({
.where((project) => project.id == id && project.content != null)
.firstOrNull;
/// The colour the project [id] shows in, its own or inherited
/// (`colorOfProject`); none: the theme's.
PaletteColor? colorOf(String id) => colorOfProject(projects, id);
/// The sections of the project [projectId] that open, in their order.
List<StoredSection> sectionsOf(String projectId) => [
for (final section in sections)

View file

@ -283,3 +283,25 @@ TaskContent completedContent(
completions: [...content.completions, now],
);
}
/// The done tasks a list's Delete completed tasks deletes (the developer's
/// review, 9 Nov 2026): those of the project [projectId], sub-tasks too,
/// and those dated from [from] to [until], each where given.
List<OpenTask> completedIn(
List<OpenTask> tasks, {
String? projectId,
Day? from,
Day? until,
}) => [
for (final task in tasks)
if (task.content.completed &&
(projectId == null || task.content.projectId == projectId) &&
(from == null && until == null ||
switch (task.content.date?.day) {
final day? =>
(from == null || day.compareTo(from) >= 0) &&
(until == null || day.compareTo(until) <= 0),
null => false,
}))
task,
];

View file

@ -141,6 +141,10 @@ class _QuickAddState() extends State<_QuickAdd> {
Day? _deadline;
var _describing = false;
/// Where the `#`, `@` or `+` stands whose suggestions Escape closed: they
/// stay closed until another one is typed.
int? _dismissedAt;
/// The title said aloud (D29), from the last button while the title is
/// empty.
late final _dictation = Dictation(
@ -517,11 +521,17 @@ class _QuickAddState() extends State<_QuickAdd> {
null => null,
};
final typing = _title.text.trim().isNotEmpty;
final suggestions = _suggestions(l10n, state);
final placing = switch (_typing()) {
final symbol = _typing();
final dismissed = symbol != null && symbol.start == _dismissedAt;
final suggestions = dismissed
? const <NamedId>[]
: _suggestions(l10n, state);
final placing = switch (symbol) {
_ when dismissed => null,
(symbol: '+', :final typed, start: _) => typed.trim(),
_ => null,
};
final suggesting = suggestions.isNotEmpty || placing != null;
final scheme = theme.colorScheme;
const borderless = InputDecoration(
border: InputBorder.none,
@ -550,10 +560,14 @@ class _QuickAddState() extends State<_QuickAdd> {
),
),
),
// The down arrow opens the description, as Todoist's.
// The down arrow opens the description, as Todoist's; Escape
// closes the suggestions first, then quick add (the route's).
CallbackShortcuts(
bindings: {
const SingleActivator(LogicalKeyboardKey.arrowDown): _describe,
if (suggesting)
const SingleActivator(LogicalKeyboardKey.escape): () =>
setState(() => _dismissedAt = symbol!.start),
},
child: TextField(
key: quickAddTitleKey,
@ -574,7 +588,7 @@ class _QuickAddState() extends State<_QuickAdd> {
onSubmitted: (_) => _add(),
),
),
if (suggestions.isNotEmpty || placing != null)
if (suggesting)
Material(
key: quickAddSuggestionsKey,
color: scheme.surfaceContainerHighest,
@ -706,6 +720,15 @@ class _QuickAddState() extends State<_QuickAdd> {
? LucideIcons.inbox
: LucideIcons.hash,
tooltip: l10n.projectPickTitle,
// In the project's colour, as Browse lists it.
color: switch (project) {
final project? when !project.inbox =>
switch (state.colorOf(project.id)) {
final color? => paletteText(context, color),
null => null,
},
_ => null,
},
label: switch (project) {
final project? when !project.inbox =>
project.content!.name,

View file

@ -47,7 +47,7 @@ Future<String?> pickProject(BuildContext context, String? current) {
ListTile(
leading: Icon(
project.inbox ? LucideIcons.inbox : LucideIcons.hash,
color: switch (content.color) {
color: switch (state.colorOf(project.id)) {
final color? => paletteText(context, color),
null => null,
},

View file

@ -151,13 +151,82 @@ class const TaskSelectionBar({
}
}
/// The menu of the task [task], opened by a right click at [position]
/// (the developer's review, 9 Nov 2026): what the selection bar does, for
/// it alone, without choosing it.
Future<void> showTaskMenu(
BuildContext context,
OpenTask task,
Offset position,
) async {
final l10n = AppLocalizations.of(context);
final actions = _SelectionActions(
context,
TaskSelection()..toggle(task.id),
one: true,
);
final overlay = Overlay.of(context).context.findRenderObject()! as RenderBox;
final action = await showMenu<Future<void> Function()>(
context: context,
position: RelativeRect.fromRect(
position & Size.zero,
Offset.zero & overlay.size,
),
items: [
if (!task.content.completed)
PopupMenuItem(
value: actions.complete,
child: MenuEntry(LucideIcons.circleCheck, l10n.taskComplete),
),
PopupMenuItem(
value: actions.date,
child: MenuEntry(LucideIcons.calendar, l10n.selectionDate),
),
PopupMenuItem(
value: actions.move,
child: MenuEntry(LucideIcons.folderInput, l10n.selectionMove),
),
PopupMenuItem(
value: actions.labels,
child: MenuEntry(LucideIcons.tag, l10n.selectionLabels),
),
PopupMenuItem(
value: actions.priority,
child: MenuEntry(LucideIcons.flag, l10n.selectionPriority),
),
PopupMenuItem(
value: actions.deadline,
child: MenuEntry(LucideIcons.target, l10n.selectionDeadline),
),
PopupMenuItem(
value: actions.duplicate,
child: MenuEntry(LucideIcons.copyPlus, l10n.taskDuplicate),
),
const PopupMenuDivider(),
PopupMenuItem(
value: actions.delete,
child: MenuEntry(
LucideIcons.trash2,
l10n.taskDelete,
destructive: true,
),
),
],
);
if (action != null && context.mounted) {
await action();
}
}
/// What the bar does to the chosen tasks, written through the person's
/// [TaskRepository] together: shown at once, as choosing ends, and shown
/// as they were again, with a snack bar, where writing fails.
/// as they were again, with a snack bar, where writing fails; for a task's
/// own menu ([one]), a selection of it alone that nobody sees.
class _SelectionActions(
final BuildContext _context,
final TaskSelection _selection,
) {
final TaskSelection _selection, {
final bool one = false,
}) {
AppLocalizations get _l10n => AppLocalizations.of(_context);
TaskRepository get _tasks => PersonScope.of<TodoPerson>(_context).tasks;
@ -309,11 +378,20 @@ class _SelectionActions(
Future<void> delete() async {
final l10n = _l10n;
final count = _selection.ids.length;
final single = switch (_chosen) {
[final task] when one => task,
_ => null,
};
if (await confirmDestructiveAction(
_context,
title: l10n.selectionDeleteTitle,
message: l10n.selectionDeleteMessage(count),
confirmLabel: l10n.selectionDelete(count),
title: single == null ? l10n.selectionDeleteTitle : l10n.taskDeleteTitle,
message: switch (single) {
final task? => l10n.taskDeleteMessage(task.content.title),
null => l10n.selectionDeleteMessage(count),
},
confirmLabel: single == null
? l10n.selectionDelete(count)
: l10n.taskDelete,
)) {
final ids = _selection.ids.toList();
final tasks = _tasks;

View file

@ -545,12 +545,13 @@ class _TaskSheetState() extends State<_TaskSheet> {
),
),
?action,
if (action == null)
IconButton(
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
icon: const Icon(LucideIcons.x),
onPressed: _closer(context),
),
// Beside the menu in the pane, so closing it is a click (the
// developer's review, 9 Nov 2026).
IconButton(
tooltip: MaterialLocalizations.of(context).closeButtonTooltip,
icon: const Icon(LucideIcons.x),
onPressed: _closer(context),
),
],
),
),
@ -566,8 +567,9 @@ class _TaskSheetState() extends State<_TaskSheet> {
}
/// The task's menu (the developer's, 1 Nov, in place of the close
/// button): when it was added, Duplicate, hiding or showing its done
/// sub-tasks, Close in the pane, and deleting it, which asks first.
/// button, which the pane shows beside it since 9 Nov): when it was
/// added, Duplicate, hiding or showing its done sub-tasks, and deleting
/// it, which asks first.
Widget _menu(BuildContext context, OpenTask task, List<OpenTask> subTasks) {
final l10n = AppLocalizations.of(context);
final content = task.content;
@ -624,11 +626,6 @@ class _TaskSheetState() extends State<_TaskSheet> {
: l10n.subTasksHideDone,
),
),
if (widget.inPane)
PopupMenuItem(
value: () async => _closer(context)(),
child: MenuEntry(LucideIcons.x, l10n.taskClose),
),
const PopupMenuDivider(),
PopupMenuItem(
value: () async {
@ -1025,7 +1022,7 @@ class const _TaskPath({
Icon(
project.inbox ? LucideIcons.inbox : LucideIcons.hash,
size: 18,
color: switch (project.content?.color) {
color: switch (state.colorOf(project.id)) {
final color? => paletteText(context, color),
null => muted,
},

View file

@ -212,7 +212,7 @@ class const TaskTile({
),
)
: TaskCheck(task: task);
final tile = ListTile(
Widget tile = ListTile(
selected: chosen,
selectedTileColor: theme.colorScheme.secondaryContainer,
selectedColor: theme.colorScheme.onSecondaryContainer,
@ -268,6 +268,15 @@ class const TaskTile({
null => () => openTaskSheet(context, task.id),
},
);
// A right click opens the task's menu, unless tasks are being chosen
// (the developer's review, 9 Nov 2026).
if (selection == null || !selection.active) {
tile = GestureDetector(
onSecondaryTapUp: (details) =>
unawaited(showTaskMenu(context, task, details.globalPosition)),
child: tile,
);
}
if (selection == null) {
return _boxed(context, tile);
}

View file

@ -93,7 +93,7 @@ class _BrowseTabState() extends State<BrowseTab> {
),
leading: Icon(
LucideIcons.hash,
color: switch (project.content!.color) {
color: switch (state.colorOf(project.id)) {
final color? => paletteText(context, color),
null => null,
},

View file

@ -745,8 +745,7 @@ class const _TaskBlock({
for (final label in state.labels)
if (label.id == id) ?label.content?.color,
].firstOrNull,
TaskColoring.project =>
state.projectOf(content.projectId)?.content?.color,
TaskColoring.project => state.colorOf(content.projectId),
};
final color =
switch (palette) {

View file

@ -387,7 +387,7 @@ Future<void> _pickProjects(
project.inbox
? LucideIcons.inbox
: LucideIcons.hash,
color: switch (project.content?.color) {
color: switch (state.colorOf(project.id)) {
final color? => paletteText(context, color),
null => null,
},

Some files were not shown because too many files have changed in this diff Show more