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:
parent
560de2ed93
commit
9e1442d89e
26 changed files with 810 additions and 78 deletions
|
|
@ -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}}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"@@locale": "en_US",
|
||||
"projectFavorite": "Favorite",
|
||||
"favoritesTitle": "Favorites"
|
||||
"favoritesTitle": "Favorites",
|
||||
"projectInheritColor": "Same color as its parent"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue