feat(todo): Today, Upcoming, Inbox, projects, quick add and the task sheet (ADR 0033, D8, D9)
The tabs hold the person's tasks: Today what is overdue (Reschedule moving it all to a day, times kept) and what is due today; Upcoming the overdue ones and the next four weeks day by day; Inbox and each project's screen from Browse the tasks above and in their sections, done ones on asking; Browse the projects with their open counts and the labels. A task's row shows its check in its priority's colour, its date in the colour of how near it is, its deadline, its sub-tasks done and its labels; quick add takes its title, date, priority, project, labels and description and stays for the next; its sheet edits all of it, adds sub-tasks and deletes it with them after asking. Colours are the palette's, cerise for overdue and P1 against the strawberry wash. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
bd3710ae58
commit
030c62ddde
26 changed files with 3647 additions and 121 deletions
|
|
@ -15,11 +15,84 @@
|
|||
"shellTabUpcoming": "Demnächst",
|
||||
"shellTabInbox": "Eingang",
|
||||
"shellTabBrowse": "Übersicht",
|
||||
"todayLaterTitle": "Hier stehen die Aufgaben von heute",
|
||||
"upcomingLaterTitle": "Hier stehen die nächsten Tage",
|
||||
"inboxLaterTitle": "Hier stehen Aufgaben ohne Projekt",
|
||||
"browseLaterTitle": "Hier stehen deine Projekte und Labels",
|
||||
"tasksLaterHint": "Aufgaben kommen mit einer späteren Version von Halkyon Todo.",
|
||||
"settingsTitle": "Einstellungen",
|
||||
"settingsVersion": "Version {version} (Build {build})"
|
||||
"settingsVersion": "Version {version} (Build {build})",
|
||||
|
||||
"clear": "Entfernen",
|
||||
"colorLabel": "Farbe",
|
||||
"completedShow": "Erledigte zeigen",
|
||||
"completedHide": "Erledigte ausblenden",
|
||||
"dateTitle": "Datum",
|
||||
"dateNextWeek": "Nächste Woche",
|
||||
"dateWeekend": "Dieses Wochenende",
|
||||
"datePick": "Tag wählen",
|
||||
"dateTime": "Uhrzeit",
|
||||
"dateNone": "Kein Datum",
|
||||
"dateNoneYet": "Kein Datum",
|
||||
"dayToday": "Heute",
|
||||
"dayTomorrow": "Morgen",
|
||||
"dayYesterday": "Gestern",
|
||||
"dayHeadingNamed": "{date} · {name}",
|
||||
"deadlineOn": "Frist {day}",
|
||||
"deadlineNone": "Keine Frist",
|
||||
"keysMissing": "Dieses Gerät kann nichts speichern: Es hat die Schlüssel deines Kontos nicht. Melde dich mit deinem Passwort oder deiner Wiederherstellungsphrase an, um sie hinzuzufügen.",
|
||||
"labelsTitle": "Labels",
|
||||
"labelsNone": "Noch keine Labels.",
|
||||
"labelsNoneSet": "Keine Labels",
|
||||
"labelsCount": "{count, plural, =1{1 Label} other{{count} Labels}}",
|
||||
"labelAdd": "Neues Label",
|
||||
"labelEdit": "Label bearbeiten",
|
||||
"labelDelete": "Label löschen",
|
||||
"labelDeleteTitle": "Label löschen?",
|
||||
"labelDeleteMessage": "„{name}“ wird von jeder Aufgabe entfernt, die es hat.",
|
||||
"nameLabel": "Name",
|
||||
"save": "Speichern",
|
||||
"overdue": "Überfällig",
|
||||
"reschedule": "Verschieben",
|
||||
"priorityTitle": "Priorität",
|
||||
"priority1": "Priorität 1",
|
||||
"priority2": "Priorität 2",
|
||||
"priority3": "Priorität 3",
|
||||
"priority4": "Keine Priorität",
|
||||
"projectsTitle": "Projekte",
|
||||
"projectsNone": "Noch keine eigenen Projekte.",
|
||||
"projectAdd": "Neues Projekt",
|
||||
"projectEdit": "Projekt bearbeiten",
|
||||
"projectDelete": "Projekt löschen",
|
||||
"projectDeleteTitle": "Projekt löschen?",
|
||||
"projectDeleteMessage": "„{name}“ wird mit seinen Abschnitten und Aufgaben auf all deinen Geräten gelöscht.",
|
||||
"projectEmptyTitle": "Hier ist nichts zu tun",
|
||||
"projectEmptyHint": "Füge mit dem Plus eine Aufgabe hinzu.",
|
||||
"projectGone": "Dieses Projekt wurde gelöscht.",
|
||||
"projectMenu": "Mehr",
|
||||
"projectPickTitle": "Projekt",
|
||||
"sectionAdd": "Neuer Abschnitt",
|
||||
"sectionEdit": "Abschnitt umbenennen",
|
||||
"sectionDelete": "Abschnitt löschen",
|
||||
"sectionDeleteTitle": "Abschnitt löschen?",
|
||||
"sectionDeleteMessage": "„{name}“ wird mit seinen Aufgaben gelöscht.",
|
||||
"sectionMenu": "Abschnitt",
|
||||
"subTasks": "Unteraufgaben",
|
||||
"subTasksDone": "Unteraufgaben {done}/{count}",
|
||||
"subTaskAdd": "Unteraufgabe hinzufügen",
|
||||
"taskTitle": "Aufgabe",
|
||||
"taskAddTitle": "Aufgabe hinzufügen",
|
||||
"taskAdd": "Hinzufügen",
|
||||
"taskAddOn": "Aufgabe für {day} hinzufügen",
|
||||
"taskTitleHint": "Was ist zu tun?",
|
||||
"taskDescription": "Beschreibung",
|
||||
"taskDescriptionHint": "Beschreibung",
|
||||
"taskComplete": "Als erledigt markieren",
|
||||
"taskReopen": "Als unerledigt markieren",
|
||||
"taskCompleted": "Aufgabe erledigt",
|
||||
"taskDelete": "Aufgabe löschen",
|
||||
"taskDeleteTitle": "Aufgabe löschen?",
|
||||
"taskDeleteMessage": "„{title}“ wird auf all deinen Geräten gelöscht.",
|
||||
"taskDeleteWithSubTasks": "„{title}“ wird mit {count, plural, =1{seiner Unteraufgabe} other{seinen {count} Unteraufgaben}} auf all deinen Geräten gelöscht.",
|
||||
"taskGone": "Diese Aufgabe wurde gelöscht.",
|
||||
"todayEmptyTitle": "Heute ist nichts fällig",
|
||||
"todayEmptyHint": "Aufgaben mit dem heutigen Datum und überfällige erscheinen hier.",
|
||||
"lockedRows": "{count, plural, =1{1 Eintrag lässt sich auf diesem Gerät nicht öffnen.} other{{count} Einträge lassen sich auf diesem Gerät nicht öffnen.}}",
|
||||
"inboxMissingTitle": "Noch kein Eingang",
|
||||
"inboxMissingHint": "Dein Eingang kommt mit dem nächsten Abgleich, oder sobald dieses Gerät die Schlüssel deines Kontos hat."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,12 +17,96 @@
|
|||
"shellTabUpcoming": "Upcoming",
|
||||
"shellTabInbox": "Inbox",
|
||||
"shellTabBrowse": "Browse",
|
||||
"todayLaterTitle": "Today's tasks come here",
|
||||
"upcomingLaterTitle": "The days ahead come here",
|
||||
"inboxLaterTitle": "Tasks without a project come here",
|
||||
"browseLaterTitle": "Your projects and labels come here",
|
||||
"tasksLaterHint": "Tasks come with a later version of Halkyon Todo.",
|
||||
"settingsTitle": "Settings",
|
||||
"settingsVersion": "Version {version} (build {build})",
|
||||
"@settingsVersion": {"placeholders": {"version": {"type": "String"}, "build": {"type": "String"}}}
|
||||
"@settingsVersion": {"placeholders": {"version": {"type": "String"}, "build": {"type": "String"}}},
|
||||
|
||||
"clear": "Clear",
|
||||
"colorLabel": "Colour",
|
||||
"completedShow": "Show done tasks",
|
||||
"completedHide": "Hide done tasks",
|
||||
"dateTitle": "Date",
|
||||
"dateNextWeek": "Next week",
|
||||
"dateWeekend": "This weekend",
|
||||
"datePick": "Pick a day",
|
||||
"dateTime": "Time",
|
||||
"dateNone": "No date",
|
||||
"dateNoneYet": "No date",
|
||||
"dayToday": "Today",
|
||||
"dayTomorrow": "Tomorrow",
|
||||
"dayYesterday": "Yesterday",
|
||||
"dayHeadingNamed": "{date} · {name}",
|
||||
"@dayHeadingNamed": {"placeholders": {"date": {"type": "String"}, "name": {"type": "String"}}},
|
||||
"deadlineOn": "Deadline {day}",
|
||||
"@deadlineOn": {"placeholders": {"day": {"type": "String"}}},
|
||||
"deadlineNone": "No deadline",
|
||||
"keysMissing": "This device can't save: it doesn't hold your account's keys. Sign in with your password or recovery phrase to add them.",
|
||||
"labelsTitle": "Labels",
|
||||
"labelsNone": "No labels yet.",
|
||||
"labelsNoneSet": "No labels",
|
||||
"labelsCount": "{count, plural, =1{1 label} other{{count} labels}}",
|
||||
"@labelsCount": {"placeholders": {"count": {"type": "int"}}},
|
||||
"labelAdd": "New label",
|
||||
"labelEdit": "Edit label",
|
||||
"labelDelete": "Delete label",
|
||||
"labelDeleteTitle": "Delete the label?",
|
||||
"labelDeleteMessage": "\"{name}\" is taken off every task that has it.",
|
||||
"@labelDeleteMessage": {"placeholders": {"name": {"type": "String"}}},
|
||||
"nameLabel": "Name",
|
||||
"save": "Save",
|
||||
"overdue": "Overdue",
|
||||
"reschedule": "Reschedule",
|
||||
"priorityTitle": "Priority",
|
||||
"priority1": "Priority 1",
|
||||
"priority2": "Priority 2",
|
||||
"priority3": "Priority 3",
|
||||
"priority4": "No priority",
|
||||
"projectsTitle": "Projects",
|
||||
"projectsNone": "No projects of your own yet.",
|
||||
"projectAdd": "New project",
|
||||
"projectEdit": "Edit project",
|
||||
"projectDelete": "Delete project",
|
||||
"projectDeleteTitle": "Delete the project?",
|
||||
"projectDeleteMessage": "\"{name}\" is deleted with its sections and tasks, on all your devices.",
|
||||
"@projectDeleteMessage": {"placeholders": {"name": {"type": "String"}}},
|
||||
"projectEmptyTitle": "Nothing to do here",
|
||||
"projectEmptyHint": "Add a task with the + button.",
|
||||
"projectGone": "This project was deleted.",
|
||||
"projectMenu": "More",
|
||||
"projectPickTitle": "Project",
|
||||
"sectionAdd": "New section",
|
||||
"sectionEdit": "Rename section",
|
||||
"sectionDelete": "Delete section",
|
||||
"sectionDeleteTitle": "Delete the section?",
|
||||
"sectionDeleteMessage": "\"{name}\" is deleted with its tasks.",
|
||||
"@sectionDeleteMessage": {"placeholders": {"name": {"type": "String"}}},
|
||||
"sectionMenu": "Section",
|
||||
"subTasks": "Sub-tasks",
|
||||
"subTasksDone": "Sub-tasks {done}/{count}",
|
||||
"@subTasksDone": {"placeholders": {"done": {"type": "int"}, "count": {"type": "int"}}},
|
||||
"subTaskAdd": "Add sub-task",
|
||||
"taskTitle": "Task",
|
||||
"taskAddTitle": "Add task",
|
||||
"taskAdd": "Add",
|
||||
"taskAddOn": "Add a task on {day}",
|
||||
"@taskAddOn": {"placeholders": {"day": {"type": "String"}}},
|
||||
"taskTitleHint": "What's to be done?",
|
||||
"taskDescription": "Description",
|
||||
"taskDescriptionHint": "Description",
|
||||
"taskComplete": "Mark as done",
|
||||
"taskReopen": "Mark as not done",
|
||||
"taskCompleted": "Task done",
|
||||
"taskDelete": "Delete task",
|
||||
"taskDeleteTitle": "Delete the task?",
|
||||
"taskDeleteMessage": "\"{title}\" is deleted on all your devices.",
|
||||
"@taskDeleteMessage": {"placeholders": {"title": {"type": "String"}}},
|
||||
"taskDeleteWithSubTasks": "\"{title}\" is deleted with its {count, plural, =1{sub-task} other{{count} sub-tasks}}, on all your devices.",
|
||||
"@taskDeleteWithSubTasks": {"placeholders": {"title": {"type": "String"}, "count": {"type": "int"}}},
|
||||
"taskGone": "This task was deleted.",
|
||||
"todayEmptyTitle": "Nothing due today",
|
||||
"todayEmptyHint": "Tasks dated today, and those overdue, show here.",
|
||||
"lockedRows": "{count, plural, =1{1 item doesn't open on this device.} other{{count} items don't open on this device.}}",
|
||||
"@lockedRows": {"placeholders": {"count": {"type": "int"}}},
|
||||
"inboxMissingTitle": "No Inbox yet",
|
||||
"inboxMissingHint": "Your Inbox comes with the next sync, or once this device holds your account's keys."
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,36 +160,6 @@ abstract class AppLocalizations {
|
|||
/// **'Browse'**
|
||||
String get shellTabBrowse;
|
||||
|
||||
/// No description provided for @todayLaterTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today\'s tasks come here'**
|
||||
String get todayLaterTitle;
|
||||
|
||||
/// No description provided for @upcomingLaterTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The days ahead come here'**
|
||||
String get upcomingLaterTitle;
|
||||
|
||||
/// No description provided for @inboxLaterTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tasks without a project come here'**
|
||||
String get inboxLaterTitle;
|
||||
|
||||
/// No description provided for @browseLaterTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your projects and labels come here'**
|
||||
String get browseLaterTitle;
|
||||
|
||||
/// No description provided for @tasksLaterHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tasks come with a later version of Halkyon Todo.'**
|
||||
String get tasksLaterHint;
|
||||
|
||||
/// No description provided for @settingsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -201,6 +171,468 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Version {version} (build {build})'**
|
||||
String settingsVersion(String version, String build);
|
||||
|
||||
/// No description provided for @clear.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear'**
|
||||
String get clear;
|
||||
|
||||
/// No description provided for @colorLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Colour'**
|
||||
String get colorLabel;
|
||||
|
||||
/// No description provided for @completedShow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show done tasks'**
|
||||
String get completedShow;
|
||||
|
||||
/// No description provided for @completedHide.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hide done tasks'**
|
||||
String get completedHide;
|
||||
|
||||
/// No description provided for @dateTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Date'**
|
||||
String get dateTitle;
|
||||
|
||||
/// No description provided for @dateNextWeek.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Next week'**
|
||||
String get dateNextWeek;
|
||||
|
||||
/// No description provided for @dateWeekend.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This weekend'**
|
||||
String get dateWeekend;
|
||||
|
||||
/// No description provided for @datePick.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pick a day'**
|
||||
String get datePick;
|
||||
|
||||
/// No description provided for @dateTime.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Time'**
|
||||
String get dateTime;
|
||||
|
||||
/// No description provided for @dateNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No date'**
|
||||
String get dateNone;
|
||||
|
||||
/// No description provided for @dateNoneYet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No date'**
|
||||
String get dateNoneYet;
|
||||
|
||||
/// No description provided for @dayToday.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Today'**
|
||||
String get dayToday;
|
||||
|
||||
/// No description provided for @dayTomorrow.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tomorrow'**
|
||||
String get dayTomorrow;
|
||||
|
||||
/// No description provided for @dayYesterday.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Yesterday'**
|
||||
String get dayYesterday;
|
||||
|
||||
/// No description provided for @dayHeadingNamed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{date} · {name}'**
|
||||
String dayHeadingNamed(String date, String name);
|
||||
|
||||
/// No description provided for @deadlineOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Deadline {day}'**
|
||||
String deadlineOn(String day);
|
||||
|
||||
/// No description provided for @deadlineNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No deadline'**
|
||||
String get deadlineNone;
|
||||
|
||||
/// No description provided for @keysMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This device can\'t save: it doesn\'t hold your account\'s keys. Sign in with your password or recovery phrase to add them.'**
|
||||
String get keysMissing;
|
||||
|
||||
/// No description provided for @labelsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Labels'**
|
||||
String get labelsTitle;
|
||||
|
||||
/// No description provided for @labelsNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No labels yet.'**
|
||||
String get labelsNone;
|
||||
|
||||
/// No description provided for @labelsNoneSet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No labels'**
|
||||
String get labelsNoneSet;
|
||||
|
||||
/// No description provided for @labelsCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 label} other{{count} labels}}'**
|
||||
String labelsCount(int count);
|
||||
|
||||
/// No description provided for @labelAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New label'**
|
||||
String get labelAdd;
|
||||
|
||||
/// No description provided for @labelEdit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit label'**
|
||||
String get labelEdit;
|
||||
|
||||
/// No description provided for @labelDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete label'**
|
||||
String get labelDelete;
|
||||
|
||||
/// No description provided for @labelDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete the label?'**
|
||||
String get labelDeleteTitle;
|
||||
|
||||
/// No description provided for @labelDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'\"{name}\" is taken off every task that has it.'**
|
||||
String labelDeleteMessage(String name);
|
||||
|
||||
/// No description provided for @nameLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Name'**
|
||||
String get nameLabel;
|
||||
|
||||
/// No description provided for @save.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Save'**
|
||||
String get save;
|
||||
|
||||
/// No description provided for @overdue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Overdue'**
|
||||
String get overdue;
|
||||
|
||||
/// No description provided for @reschedule.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reschedule'**
|
||||
String get reschedule;
|
||||
|
||||
/// No description provided for @priorityTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Priority'**
|
||||
String get priorityTitle;
|
||||
|
||||
/// No description provided for @priority1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Priority 1'**
|
||||
String get priority1;
|
||||
|
||||
/// No description provided for @priority2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Priority 2'**
|
||||
String get priority2;
|
||||
|
||||
/// No description provided for @priority3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Priority 3'**
|
||||
String get priority3;
|
||||
|
||||
/// No description provided for @priority4.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No priority'**
|
||||
String get priority4;
|
||||
|
||||
/// No description provided for @projectsTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Projects'**
|
||||
String get projectsTitle;
|
||||
|
||||
/// No description provided for @projectsNone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No projects of your own yet.'**
|
||||
String get projectsNone;
|
||||
|
||||
/// No description provided for @projectAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New project'**
|
||||
String get projectAdd;
|
||||
|
||||
/// No description provided for @projectEdit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Edit project'**
|
||||
String get projectEdit;
|
||||
|
||||
/// No description provided for @projectDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete project'**
|
||||
String get projectDelete;
|
||||
|
||||
/// No description provided for @projectDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete the project?'**
|
||||
String get projectDeleteTitle;
|
||||
|
||||
/// No description provided for @projectDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'\"{name}\" is deleted with its sections and tasks, on all your devices.'**
|
||||
String projectDeleteMessage(String name);
|
||||
|
||||
/// No description provided for @projectEmptyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nothing to do here'**
|
||||
String get projectEmptyTitle;
|
||||
|
||||
/// No description provided for @projectEmptyHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a task with the + button.'**
|
||||
String get projectEmptyHint;
|
||||
|
||||
/// No description provided for @projectGone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This project was deleted.'**
|
||||
String get projectGone;
|
||||
|
||||
/// No description provided for @projectMenu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'More'**
|
||||
String get projectMenu;
|
||||
|
||||
/// No description provided for @projectPickTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Project'**
|
||||
String get projectPickTitle;
|
||||
|
||||
/// No description provided for @sectionAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'New section'**
|
||||
String get sectionAdd;
|
||||
|
||||
/// No description provided for @sectionEdit.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Rename section'**
|
||||
String get sectionEdit;
|
||||
|
||||
/// No description provided for @sectionDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete section'**
|
||||
String get sectionDelete;
|
||||
|
||||
/// No description provided for @sectionDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete the section?'**
|
||||
String get sectionDeleteTitle;
|
||||
|
||||
/// No description provided for @sectionDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'\"{name}\" is deleted with its tasks.'**
|
||||
String sectionDeleteMessage(String name);
|
||||
|
||||
/// No description provided for @sectionMenu.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Section'**
|
||||
String get sectionMenu;
|
||||
|
||||
/// No description provided for @subTasks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sub-tasks'**
|
||||
String get subTasks;
|
||||
|
||||
/// No description provided for @subTasksDone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sub-tasks {done}/{count}'**
|
||||
String subTasksDone(int done, int count);
|
||||
|
||||
/// No description provided for @subTaskAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add sub-task'**
|
||||
String get subTaskAdd;
|
||||
|
||||
/// No description provided for @taskTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Task'**
|
||||
String get taskTitle;
|
||||
|
||||
/// No description provided for @taskAddTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add task'**
|
||||
String get taskAddTitle;
|
||||
|
||||
/// No description provided for @taskAdd.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add'**
|
||||
String get taskAdd;
|
||||
|
||||
/// No description provided for @taskAddOn.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add a task on {day}'**
|
||||
String taskAddOn(String day);
|
||||
|
||||
/// No description provided for @taskTitleHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'What\'s to be done?'**
|
||||
String get taskTitleHint;
|
||||
|
||||
/// No description provided for @taskDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Description'**
|
||||
String get taskDescription;
|
||||
|
||||
/// No description provided for @taskDescriptionHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Description'**
|
||||
String get taskDescriptionHint;
|
||||
|
||||
/// No description provided for @taskComplete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mark as done'**
|
||||
String get taskComplete;
|
||||
|
||||
/// No description provided for @taskReopen.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Mark as not done'**
|
||||
String get taskReopen;
|
||||
|
||||
/// No description provided for @taskCompleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Task done'**
|
||||
String get taskCompleted;
|
||||
|
||||
/// No description provided for @taskDelete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete task'**
|
||||
String get taskDelete;
|
||||
|
||||
/// No description provided for @taskDeleteTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete the task?'**
|
||||
String get taskDeleteTitle;
|
||||
|
||||
/// No description provided for @taskDeleteMessage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'\"{title}\" is deleted on all your devices.'**
|
||||
String taskDeleteMessage(String title);
|
||||
|
||||
/// No description provided for @taskDeleteWithSubTasks.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'\"{title}\" is deleted with its {count, plural, =1{sub-task} other{{count} sub-tasks}}, on all your devices.'**
|
||||
String taskDeleteWithSubTasks(String title, int count);
|
||||
|
||||
/// No description provided for @taskGone.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This task was deleted.'**
|
||||
String get taskGone;
|
||||
|
||||
/// No description provided for @todayEmptyTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Nothing due today'**
|
||||
String get todayEmptyTitle;
|
||||
|
||||
/// No description provided for @todayEmptyHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tasks dated today, and those overdue, show here.'**
|
||||
String get todayEmptyHint;
|
||||
|
||||
/// No description provided for @lockedRows.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count, plural, =1{1 item doesn\'t open on this device.} other{{count} items don\'t open on this device.}}'**
|
||||
String lockedRows(int count);
|
||||
|
||||
/// No description provided for @inboxMissingTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No Inbox yet'**
|
||||
String get inboxMissingTitle;
|
||||
|
||||
/// No description provided for @inboxMissingHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Your Inbox comes with the next sync, or once this device holds your account\'s keys.'**
|
||||
String get inboxMissingHint;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -41,22 +41,6 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get shellTabBrowse => 'Übersicht';
|
||||
|
||||
@override
|
||||
String get todayLaterTitle => 'Hier stehen die Aufgaben von heute';
|
||||
|
||||
@override
|
||||
String get upcomingLaterTitle => 'Hier stehen die nächsten Tage';
|
||||
|
||||
@override
|
||||
String get inboxLaterTitle => 'Hier stehen Aufgaben ohne Projekt';
|
||||
|
||||
@override
|
||||
String get browseLaterTitle => 'Hier stehen deine Projekte und Labels';
|
||||
|
||||
@override
|
||||
String get tasksLaterHint =>
|
||||
'Aufgaben kommen mit einer späteren Version von Halkyon Todo.';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Einstellungen';
|
||||
|
||||
|
|
@ -64,4 +48,278 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
String settingsVersion(String version, String build) {
|
||||
return 'Version $version (Build $build)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get clear => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get colorLabel => 'Farbe';
|
||||
|
||||
@override
|
||||
String get completedShow => 'Erledigte zeigen';
|
||||
|
||||
@override
|
||||
String get completedHide => 'Erledigte ausblenden';
|
||||
|
||||
@override
|
||||
String get dateTitle => 'Datum';
|
||||
|
||||
@override
|
||||
String get dateNextWeek => 'Nächste Woche';
|
||||
|
||||
@override
|
||||
String get dateWeekend => 'Dieses Wochenende';
|
||||
|
||||
@override
|
||||
String get datePick => 'Tag wählen';
|
||||
|
||||
@override
|
||||
String get dateTime => 'Uhrzeit';
|
||||
|
||||
@override
|
||||
String get dateNone => 'Kein Datum';
|
||||
|
||||
@override
|
||||
String get dateNoneYet => 'Kein Datum';
|
||||
|
||||
@override
|
||||
String get dayToday => 'Heute';
|
||||
|
||||
@override
|
||||
String get dayTomorrow => 'Morgen';
|
||||
|
||||
@override
|
||||
String get dayYesterday => 'Gestern';
|
||||
|
||||
@override
|
||||
String dayHeadingNamed(String date, String name) {
|
||||
return '$date · $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String deadlineOn(String day) {
|
||||
return 'Frist $day';
|
||||
}
|
||||
|
||||
@override
|
||||
String get deadlineNone => 'Keine Frist';
|
||||
|
||||
@override
|
||||
String get keysMissing =>
|
||||
'Dieses Gerät kann nichts speichern: Es hat die Schlüssel deines Kontos nicht. Melde dich mit deinem Passwort oder deiner Wiederherstellungsphrase an, um sie hinzuzufügen.';
|
||||
|
||||
@override
|
||||
String get labelsTitle => 'Labels';
|
||||
|
||||
@override
|
||||
String get labelsNone => 'Noch keine Labels.';
|
||||
|
||||
@override
|
||||
String get labelsNoneSet => 'Keine Labels';
|
||||
|
||||
@override
|
||||
String labelsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Labels',
|
||||
one: '1 Label',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get labelAdd => 'Neues Label';
|
||||
|
||||
@override
|
||||
String get labelEdit => 'Label bearbeiten';
|
||||
|
||||
@override
|
||||
String get labelDelete => 'Label löschen';
|
||||
|
||||
@override
|
||||
String get labelDeleteTitle => 'Label löschen?';
|
||||
|
||||
@override
|
||||
String labelDeleteMessage(String name) {
|
||||
return '„$name“ wird von jeder Aufgabe entfernt, die es hat.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nameLabel => 'Name';
|
||||
|
||||
@override
|
||||
String get save => 'Speichern';
|
||||
|
||||
@override
|
||||
String get overdue => 'Überfällig';
|
||||
|
||||
@override
|
||||
String get reschedule => 'Verschieben';
|
||||
|
||||
@override
|
||||
String get priorityTitle => 'Priorität';
|
||||
|
||||
@override
|
||||
String get priority1 => 'Priorität 1';
|
||||
|
||||
@override
|
||||
String get priority2 => 'Priorität 2';
|
||||
|
||||
@override
|
||||
String get priority3 => 'Priorität 3';
|
||||
|
||||
@override
|
||||
String get priority4 => 'Keine Priorität';
|
||||
|
||||
@override
|
||||
String get projectsTitle => 'Projekte';
|
||||
|
||||
@override
|
||||
String get projectsNone => 'Noch keine eigenen Projekte.';
|
||||
|
||||
@override
|
||||
String get projectAdd => 'Neues Projekt';
|
||||
|
||||
@override
|
||||
String get projectEdit => 'Projekt bearbeiten';
|
||||
|
||||
@override
|
||||
String get projectDelete => 'Projekt löschen';
|
||||
|
||||
@override
|
||||
String get projectDeleteTitle => 'Projekt löschen?';
|
||||
|
||||
@override
|
||||
String projectDeleteMessage(String name) {
|
||||
return '„$name“ wird mit seinen Abschnitten und Aufgaben auf all deinen Geräten gelöscht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get projectEmptyTitle => 'Hier ist nichts zu tun';
|
||||
|
||||
@override
|
||||
String get projectEmptyHint => 'Füge mit dem Plus eine Aufgabe hinzu.';
|
||||
|
||||
@override
|
||||
String get projectGone => 'Dieses Projekt wurde gelöscht.';
|
||||
|
||||
@override
|
||||
String get projectMenu => 'Mehr';
|
||||
|
||||
@override
|
||||
String get projectPickTitle => 'Projekt';
|
||||
|
||||
@override
|
||||
String get sectionAdd => 'Neuer Abschnitt';
|
||||
|
||||
@override
|
||||
String get sectionEdit => 'Abschnitt umbenennen';
|
||||
|
||||
@override
|
||||
String get sectionDelete => 'Abschnitt löschen';
|
||||
|
||||
@override
|
||||
String get sectionDeleteTitle => 'Abschnitt löschen?';
|
||||
|
||||
@override
|
||||
String sectionDeleteMessage(String name) {
|
||||
return '„$name“ wird mit seinen Aufgaben gelöscht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sectionMenu => 'Abschnitt';
|
||||
|
||||
@override
|
||||
String get subTasks => 'Unteraufgaben';
|
||||
|
||||
@override
|
||||
String subTasksDone(int done, int count) {
|
||||
return 'Unteraufgaben $done/$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get subTaskAdd => 'Unteraufgabe hinzufügen';
|
||||
|
||||
@override
|
||||
String get taskTitle => 'Aufgabe';
|
||||
|
||||
@override
|
||||
String get taskAddTitle => 'Aufgabe hinzufügen';
|
||||
|
||||
@override
|
||||
String get taskAdd => 'Hinzufügen';
|
||||
|
||||
@override
|
||||
String taskAddOn(String day) {
|
||||
return 'Aufgabe für $day hinzufügen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get taskTitleHint => 'Was ist zu tun?';
|
||||
|
||||
@override
|
||||
String get taskDescription => 'Beschreibung';
|
||||
|
||||
@override
|
||||
String get taskDescriptionHint => 'Beschreibung';
|
||||
|
||||
@override
|
||||
String get taskComplete => 'Als erledigt markieren';
|
||||
|
||||
@override
|
||||
String get taskReopen => 'Als unerledigt markieren';
|
||||
|
||||
@override
|
||||
String get taskCompleted => 'Aufgabe erledigt';
|
||||
|
||||
@override
|
||||
String get taskDelete => 'Aufgabe löschen';
|
||||
|
||||
@override
|
||||
String get taskDeleteTitle => 'Aufgabe löschen?';
|
||||
|
||||
@override
|
||||
String taskDeleteMessage(String title) {
|
||||
return '„$title“ wird auf all deinen Geräten gelöscht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String taskDeleteWithSubTasks(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'seinen $count Unteraufgaben',
|
||||
one: 'seiner Unteraufgabe',
|
||||
);
|
||||
return '„$title“ wird mit $_temp0 auf all deinen Geräten gelöscht.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get taskGone => 'Diese Aufgabe wurde gelöscht.';
|
||||
|
||||
@override
|
||||
String get todayEmptyTitle => 'Heute ist nichts fällig';
|
||||
|
||||
@override
|
||||
String get todayEmptyHint =>
|
||||
'Aufgaben mit dem heutigen Datum und überfällige erscheinen hier.';
|
||||
|
||||
@override
|
||||
String lockedRows(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count Einträge lassen sich auf diesem Gerät nicht öffnen.',
|
||||
one: '1 Eintrag lässt sich auf diesem Gerät nicht öffnen.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get inboxMissingTitle => 'Noch kein Eingang';
|
||||
|
||||
@override
|
||||
String get inboxMissingHint =>
|
||||
'Dein Eingang kommt mit dem nächsten Abgleich, oder sobald dieses Gerät die Schlüssel deines Kontos hat.';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,22 +41,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get shellTabBrowse => 'Browse';
|
||||
|
||||
@override
|
||||
String get todayLaterTitle => 'Today\'s tasks come here';
|
||||
|
||||
@override
|
||||
String get upcomingLaterTitle => 'The days ahead come here';
|
||||
|
||||
@override
|
||||
String get inboxLaterTitle => 'Tasks without a project come here';
|
||||
|
||||
@override
|
||||
String get browseLaterTitle => 'Your projects and labels come here';
|
||||
|
||||
@override
|
||||
String get tasksLaterHint =>
|
||||
'Tasks come with a later version of Halkyon Todo.';
|
||||
|
||||
@override
|
||||
String get settingsTitle => 'Settings';
|
||||
|
||||
|
|
@ -64,6 +48,280 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
String settingsVersion(String version, String build) {
|
||||
return 'Version $version (build $build)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get clear => 'Clear';
|
||||
|
||||
@override
|
||||
String get colorLabel => 'Colour';
|
||||
|
||||
@override
|
||||
String get completedShow => 'Show done tasks';
|
||||
|
||||
@override
|
||||
String get completedHide => 'Hide done tasks';
|
||||
|
||||
@override
|
||||
String get dateTitle => 'Date';
|
||||
|
||||
@override
|
||||
String get dateNextWeek => 'Next week';
|
||||
|
||||
@override
|
||||
String get dateWeekend => 'This weekend';
|
||||
|
||||
@override
|
||||
String get datePick => 'Pick a day';
|
||||
|
||||
@override
|
||||
String get dateTime => 'Time';
|
||||
|
||||
@override
|
||||
String get dateNone => 'No date';
|
||||
|
||||
@override
|
||||
String get dateNoneYet => 'No date';
|
||||
|
||||
@override
|
||||
String get dayToday => 'Today';
|
||||
|
||||
@override
|
||||
String get dayTomorrow => 'Tomorrow';
|
||||
|
||||
@override
|
||||
String get dayYesterday => 'Yesterday';
|
||||
|
||||
@override
|
||||
String dayHeadingNamed(String date, String name) {
|
||||
return '$date · $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String deadlineOn(String day) {
|
||||
return 'Deadline $day';
|
||||
}
|
||||
|
||||
@override
|
||||
String get deadlineNone => 'No deadline';
|
||||
|
||||
@override
|
||||
String get keysMissing =>
|
||||
'This device can\'t save: it doesn\'t hold your account\'s keys. Sign in with your password or recovery phrase to add them.';
|
||||
|
||||
@override
|
||||
String get labelsTitle => 'Labels';
|
||||
|
||||
@override
|
||||
String get labelsNone => 'No labels yet.';
|
||||
|
||||
@override
|
||||
String get labelsNoneSet => 'No labels';
|
||||
|
||||
@override
|
||||
String labelsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count labels',
|
||||
one: '1 label',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get labelAdd => 'New label';
|
||||
|
||||
@override
|
||||
String get labelEdit => 'Edit label';
|
||||
|
||||
@override
|
||||
String get labelDelete => 'Delete label';
|
||||
|
||||
@override
|
||||
String get labelDeleteTitle => 'Delete the label?';
|
||||
|
||||
@override
|
||||
String labelDeleteMessage(String name) {
|
||||
return '\"$name\" is taken off every task that has it.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get nameLabel => 'Name';
|
||||
|
||||
@override
|
||||
String get save => 'Save';
|
||||
|
||||
@override
|
||||
String get overdue => 'Overdue';
|
||||
|
||||
@override
|
||||
String get reschedule => 'Reschedule';
|
||||
|
||||
@override
|
||||
String get priorityTitle => 'Priority';
|
||||
|
||||
@override
|
||||
String get priority1 => 'Priority 1';
|
||||
|
||||
@override
|
||||
String get priority2 => 'Priority 2';
|
||||
|
||||
@override
|
||||
String get priority3 => 'Priority 3';
|
||||
|
||||
@override
|
||||
String get priority4 => 'No priority';
|
||||
|
||||
@override
|
||||
String get projectsTitle => 'Projects';
|
||||
|
||||
@override
|
||||
String get projectsNone => 'No projects of your own yet.';
|
||||
|
||||
@override
|
||||
String get projectAdd => 'New project';
|
||||
|
||||
@override
|
||||
String get projectEdit => 'Edit project';
|
||||
|
||||
@override
|
||||
String get projectDelete => 'Delete project';
|
||||
|
||||
@override
|
||||
String get projectDeleteTitle => 'Delete the project?';
|
||||
|
||||
@override
|
||||
String projectDeleteMessage(String name) {
|
||||
return '\"$name\" is deleted with its sections and tasks, on all your devices.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get projectEmptyTitle => 'Nothing to do here';
|
||||
|
||||
@override
|
||||
String get projectEmptyHint => 'Add a task with the + button.';
|
||||
|
||||
@override
|
||||
String get projectGone => 'This project was deleted.';
|
||||
|
||||
@override
|
||||
String get projectMenu => 'More';
|
||||
|
||||
@override
|
||||
String get projectPickTitle => 'Project';
|
||||
|
||||
@override
|
||||
String get sectionAdd => 'New section';
|
||||
|
||||
@override
|
||||
String get sectionEdit => 'Rename section';
|
||||
|
||||
@override
|
||||
String get sectionDelete => 'Delete section';
|
||||
|
||||
@override
|
||||
String get sectionDeleteTitle => 'Delete the section?';
|
||||
|
||||
@override
|
||||
String sectionDeleteMessage(String name) {
|
||||
return '\"$name\" is deleted with its tasks.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sectionMenu => 'Section';
|
||||
|
||||
@override
|
||||
String get subTasks => 'Sub-tasks';
|
||||
|
||||
@override
|
||||
String subTasksDone(int done, int count) {
|
||||
return 'Sub-tasks $done/$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get subTaskAdd => 'Add sub-task';
|
||||
|
||||
@override
|
||||
String get taskTitle => 'Task';
|
||||
|
||||
@override
|
||||
String get taskAddTitle => 'Add task';
|
||||
|
||||
@override
|
||||
String get taskAdd => 'Add';
|
||||
|
||||
@override
|
||||
String taskAddOn(String day) {
|
||||
return 'Add a task on $day';
|
||||
}
|
||||
|
||||
@override
|
||||
String get taskTitleHint => 'What\'s to be done?';
|
||||
|
||||
@override
|
||||
String get taskDescription => 'Description';
|
||||
|
||||
@override
|
||||
String get taskDescriptionHint => 'Description';
|
||||
|
||||
@override
|
||||
String get taskComplete => 'Mark as done';
|
||||
|
||||
@override
|
||||
String get taskReopen => 'Mark as not done';
|
||||
|
||||
@override
|
||||
String get taskCompleted => 'Task done';
|
||||
|
||||
@override
|
||||
String get taskDelete => 'Delete task';
|
||||
|
||||
@override
|
||||
String get taskDeleteTitle => 'Delete the task?';
|
||||
|
||||
@override
|
||||
String taskDeleteMessage(String title) {
|
||||
return '\"$title\" is deleted on all your devices.';
|
||||
}
|
||||
|
||||
@override
|
||||
String taskDeleteWithSubTasks(String title, int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count sub-tasks',
|
||||
one: 'sub-task',
|
||||
);
|
||||
return '\"$title\" is deleted with its $_temp0, on all your devices.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get taskGone => 'This task was deleted.';
|
||||
|
||||
@override
|
||||
String get todayEmptyTitle => 'Nothing due today';
|
||||
|
||||
@override
|
||||
String get todayEmptyHint =>
|
||||
'Tasks dated today, and those overdue, show here.';
|
||||
|
||||
@override
|
||||
String lockedRows(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count items don\'t open on this device.',
|
||||
one: '1 item doesn\'t open on this device.',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get inboxMissingTitle => 'No Inbox yet';
|
||||
|
||||
@override
|
||||
String get inboxMissingHint =>
|
||||
'Your Inbox comes with the next sync, or once this device holds your account\'s keys.';
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
|||
import 'package:halkyon_todo_flutter/src/app/todo_frame.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_languages.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_router.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
|
|
@ -90,6 +91,12 @@ class _TodoPersonAppState() extends State<TodoPersonApp> {
|
|||
create: (context) =>
|
||||
OperatingCubit(AppScope.of(context).instanceOperators),
|
||||
),
|
||||
// The person's projects, tasks and labels, for every tab and
|
||||
// sheet.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
TodoCubit(PersonScope.of<TodoPerson>(context))..open(),
|
||||
),
|
||||
// Where sync stands: Settings' card and the dot on the cog.
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ abstract final class TodoPaths() {
|
|||
/// Browse: the projects and labels, and the cog to Settings.
|
||||
static const browse = '/browse';
|
||||
|
||||
/// A project's screen, in Browse.
|
||||
static String project(String id) => '/browse/projects/$id';
|
||||
|
||||
/// Settings, over the tabs.
|
||||
static const settings = '/settings';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_paths.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_screen.dart';
|
||||
import 'package:halkyon_todo_flutter/src/settings/settings_screen.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/browse_tab.dart';
|
||||
import 'package:halkyon_todo_flutter/src/tabs/inbox_tab.dart';
|
||||
|
|
@ -49,6 +50,13 @@ GoRouter buildTodoRouter() => GoRouter(
|
|||
GoRoute(
|
||||
path: TodoPaths.browse,
|
||||
builder: (context, state) => const BrowseTab(),
|
||||
routes: [
|
||||
GoRoute(
|
||||
path: 'projects/:id',
|
||||
builder: (context, state) =>
|
||||
ProjectScreen(projectId: state.pathParameters['id']!),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import 'dart:typed_data';
|
|||
|
||||
import 'package:halkyon_todo_flutter/src/core/sealed/json_payload.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/model/label.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/data/project_payload.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
|
||||
/// The schema of a label's payload this app writes and reads.
|
||||
const labelSchemaVersion = 1;
|
||||
|
|
@ -21,7 +21,7 @@ LabelContent? decodeLabel(ByteData payload) => decodeJson(
|
|||
name: 'Label',
|
||||
read: (json) => LabelContent(
|
||||
name: json['name']! as String,
|
||||
color: paletteColorNamed(json['color'] as String?),
|
||||
color: PaletteColor.byName(json['color'] as String?),
|
||||
position: (json['position'] as num?)?.toDouble() ?? 0,
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -24,11 +24,7 @@ ProjectContent? decodeProject(ByteData payload) => decodeJson(
|
|||
name: 'Project',
|
||||
read: (json) => ProjectContent(
|
||||
name: json['name']! as String,
|
||||
color: paletteColorNamed(json['color'] as String?),
|
||||
color: PaletteColor.byName(json['color'] as String?),
|
||||
position: (json['position'] as num?)?.toDouble() ?? 0,
|
||||
),
|
||||
);
|
||||
|
||||
/// The palette's colour named [name]; none for a name it doesn't know.
|
||||
PaletteColor? paletteColorNamed(String? name) =>
|
||||
PaletteColor.values.where((color) => color.name == name).firstOrNull;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/model/label.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/model/project.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/section/model/section.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Asks for a project's name and colour (ADR 0033, D9), [current]'s to
|
||||
/// change, or a new one's; null where the person changed their mind.
|
||||
Future<ProjectContent?> editProject(
|
||||
BuildContext context, {
|
||||
ProjectContent? current,
|
||||
}) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final named = await _nameAndColor(
|
||||
context,
|
||||
title: current == null ? l10n.projectAdd : l10n.projectEdit,
|
||||
name: current?.name ?? '',
|
||||
color: current?.color ?? PaletteColor.cobalt,
|
||||
);
|
||||
return switch (named) {
|
||||
(:final name, :final color) => ProjectContent(
|
||||
name: name,
|
||||
color: color,
|
||||
position: current?.position ?? 0,
|
||||
),
|
||||
null => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Asks for a label's name and colour, [current]'s to change, or a new
|
||||
/// one's; null where the person changed their mind.
|
||||
Future<LabelContent?> editLabel(
|
||||
BuildContext context, {
|
||||
LabelContent? current,
|
||||
}) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final named = await _nameAndColor(
|
||||
context,
|
||||
title: current == null ? l10n.labelAdd : l10n.labelEdit,
|
||||
name: current?.name ?? '',
|
||||
color: current?.color ?? PaletteColor.carrot,
|
||||
);
|
||||
return switch (named) {
|
||||
(:final name, :final color) => LabelContent(
|
||||
name: name,
|
||||
color: color,
|
||||
position: current?.position ?? 0,
|
||||
),
|
||||
null => null,
|
||||
};
|
||||
}
|
||||
|
||||
/// Asks for a section's name, [current]'s to change; null where the person
|
||||
/// changed their mind.
|
||||
Future<SectionContent?> editSection(
|
||||
BuildContext context,
|
||||
SectionContent current,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final named = await _nameAndColor(
|
||||
context,
|
||||
title: current.name.isEmpty ? l10n.sectionAdd : l10n.sectionEdit,
|
||||
name: current.name,
|
||||
color: null,
|
||||
);
|
||||
return switch (named) {
|
||||
(:final name, color: _) => current.copyWith(name: name),
|
||||
null => null,
|
||||
};
|
||||
}
|
||||
|
||||
Future<({String name, PaletteColor? color})?> _nameAndColor(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String name,
|
||||
required PaletteColor? color,
|
||||
}) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return showDialog<({String name, PaletteColor? color})>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final controller = TextEditingController(text: name);
|
||||
var chosen = color;
|
||||
void done() {
|
||||
final text = controller.text.trim();
|
||||
if (text.isNotEmpty) {
|
||||
Navigator.of(context).pop((name: text, color: chosen));
|
||||
}
|
||||
}
|
||||
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text(title),
|
||||
// As wide as a dialog's field usually is: the colour picker
|
||||
// takes the width it is given.
|
||||
content: SizedBox(
|
||||
width: 320,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(labelText: l10n.nameLabel),
|
||||
onSubmitted: (_) => done(),
|
||||
),
|
||||
if (chosen case final current?) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.colorLabel,
|
||||
style: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PaletteColorPicker(
|
||||
selected: current,
|
||||
onChanged: (picked) => setState(() => chosen = picked),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
FilledButton(onPressed: done, child: Text(l10n.save)),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_dialogs.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/tabs/tab_screen.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// The screen of the project [projectId] (the Inbox's too): its tasks, a
|
||||
/// button to add one there, and a menu to show the done ones, add a
|
||||
/// section and, but for the Inbox, edit or delete the project.
|
||||
class const ProjectScreenBody({
|
||||
required final String title,
|
||||
required final String? projectId,
|
||||
final List<PopupMenuEntry<VoidCallback>> Function(BuildContext context)? menu,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<ProjectScreenBody> createState() => _ProjectScreenBodyState();
|
||||
}
|
||||
|
||||
class _ProjectScreenBodyState() extends State<ProjectScreenBody> {
|
||||
var _completed = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final projectId = widget.projectId;
|
||||
return TabScreen(
|
||||
title: widget.title,
|
||||
actions: [
|
||||
if (projectId != null)
|
||||
PopupMenuButton<VoidCallback>(
|
||||
tooltip: l10n.projectMenu,
|
||||
onSelected: (action) => action(),
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: () => setState(() => _completed = !_completed),
|
||||
child: Text(
|
||||
_completed ? l10n.completedHide : l10n.completedShow,
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: () => addSection(context, projectId),
|
||||
child: Text(l10n.sectionAdd),
|
||||
),
|
||||
...?widget.menu?.call(context),
|
||||
],
|
||||
),
|
||||
],
|
||||
floatingActionButton: projectId == null
|
||||
? null
|
||||
: AddTaskButton(onPressed: () => showQuickAddIn(context, projectId)),
|
||||
body: Column(
|
||||
children: [
|
||||
const KeysResetBanner(),
|
||||
Expanded(
|
||||
child: switch (projectId) {
|
||||
final id? => ProjectTasks(projectId: id, completed: _completed),
|
||||
// No Inbox yet: made by the first device holding the
|
||||
// account's keys, or still on its way here.
|
||||
null
|
||||
when context.select<TodoCubit, bool>(
|
||||
(cubit) => cubit.state.loaded,
|
||||
) =>
|
||||
Center(
|
||||
child: EmptyListMessage(
|
||||
icon: LucideIcons.inbox,
|
||||
title: l10n.inboxMissingTitle,
|
||||
hint: l10n.inboxMissingHint,
|
||||
),
|
||||
),
|
||||
null => const Center(child: AppLoadingIndicator()),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A project of the person's own, from Browse (ADR 0033, D2): its tasks as
|
||||
/// the Inbox's, and in its menu editing its name and colour and deleting it
|
||||
/// with its sections and tasks, which asks first. A project gone (deleted
|
||||
/// on another device) goes back.
|
||||
class const ProjectScreen({required final String projectId, super.key})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final state = context.watch<TodoCubit>().state;
|
||||
final project = state.projectOf(projectId);
|
||||
if (project == null) {
|
||||
return TabScreen(
|
||||
title: '',
|
||||
body: Center(child: Text(state.loaded ? l10n.projectGone : '')),
|
||||
);
|
||||
}
|
||||
final content = project.content!;
|
||||
return ProjectScreenBody(
|
||||
title: project.inbox ? l10n.shellTabInbox : content.name,
|
||||
projectId: projectId,
|
||||
menu: project.inbox
|
||||
? null
|
||||
: (context) => [
|
||||
PopupMenuItem(
|
||||
value: () async {
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
if (await editProject(context, current: content)
|
||||
case final edited?) {
|
||||
await todo.projects.update(projectId, edited);
|
||||
}
|
||||
},
|
||||
child: Text(l10n.projectEdit),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: () async {
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
final router = GoRouter.of(context);
|
||||
if (await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.projectDeleteTitle,
|
||||
message: l10n.projectDeleteMessage(content.name),
|
||||
confirmLabel: l10n.projectDelete,
|
||||
)) {
|
||||
await todo.deleteProject(projectId);
|
||||
router.pop();
|
||||
}
|
||||
},
|
||||
child: Text(l10n.projectDelete),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/project/widget/project_dialogs.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/section/model/section.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/quick_add_sheet.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_tile.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// A project's tasks as a list (ADR 0033, D8): those in no section first,
|
||||
/// then each section under its name, count and description, with a menu to
|
||||
/// add a task there, rename it or delete it; done tasks too where
|
||||
/// [completed]. Sub-tasks show in their task's sheet, counted on its row.
|
||||
class const ProjectTasks({
|
||||
required final String projectId,
|
||||
final bool completed = false,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final state = context.watch<TodoCubit>().state;
|
||||
if (!state.loaded) {
|
||||
return const Center(child: AppLoadingIndicator());
|
||||
}
|
||||
final loose = tasksIn(state.tasks, projectId, completed: completed);
|
||||
final sections = state.sectionsOf(projectId);
|
||||
if (loose.isEmpty &&
|
||||
sections.isEmpty &&
|
||||
!state.tasks.any((t) => t.content.projectId == projectId)) {
|
||||
return Center(
|
||||
child: EmptyListMessage(
|
||||
icon: LucideIcons.listTodo,
|
||||
title: l10n.projectEmptyTitle,
|
||||
hint: l10n.projectEmptyHint,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ListView(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 88 + FloatingNavigationBar.coverOf(context),
|
||||
),
|
||||
children: [
|
||||
for (final task in loose) TaskTile(task: task),
|
||||
for (final section in sections) ..._section(context, state, section),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _section(
|
||||
BuildContext context,
|
||||
TodoState state,
|
||||
StoredSection section,
|
||||
) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final content = section.content!;
|
||||
final tasks = tasksIn(
|
||||
state.tasks,
|
||||
projectId,
|
||||
sectionId: section.id,
|
||||
completed: completed,
|
||||
);
|
||||
return [
|
||||
const Divider(height: 24),
|
||||
ListTile(
|
||||
title: Text(
|
||||
'${content.name} ${tasks.where((t) => !t.content.completed).length}',
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
subtitle: content.description.isEmpty
|
||||
? null
|
||||
: Text(
|
||||
content.description,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: PopupMenuButton<_SectionAction>(
|
||||
tooltip: l10n.sectionMenu,
|
||||
icon: const Icon(LucideIcons.ellipsisVertical),
|
||||
onSelected: (action) async {
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
switch (action) {
|
||||
case _SectionAction.add:
|
||||
await showQuickAdd(
|
||||
context,
|
||||
projectId: projectId,
|
||||
sectionId: section.id,
|
||||
);
|
||||
case _SectionAction.edit:
|
||||
if (await editSection(context, content) case final edited?) {
|
||||
await todo.sections.update(section.id, edited);
|
||||
}
|
||||
case _SectionAction.delete:
|
||||
if (await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.sectionDeleteTitle,
|
||||
message: l10n.sectionDeleteMessage(content.name),
|
||||
confirmLabel: l10n.sectionDelete,
|
||||
)) {
|
||||
await todo.deleteSection(projectId, section.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: _SectionAction.add,
|
||||
child: Text(l10n.taskAddTitle),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SectionAction.edit,
|
||||
child: Text(l10n.sectionEdit),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: _SectionAction.delete,
|
||||
child: Text(l10n.sectionDelete),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
for (final task in tasks) TaskTile(task: task),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
enum _SectionAction() {
|
||||
add,
|
||||
edit,
|
||||
delete,
|
||||
}
|
||||
|
||||
/// Asks for a new section's name and adds it last in [projectId].
|
||||
Future<void> addSection(BuildContext context, String projectId) async {
|
||||
final todo = PersonScope.of<TodoPerson>(context);
|
||||
final sections = context.read<TodoCubit>().state.sectionsOf(projectId);
|
||||
final added = await editSection(
|
||||
context,
|
||||
SectionContent(
|
||||
name: '',
|
||||
projectId: projectId,
|
||||
position: sections.isEmpty ? 0 : sections.last.content!.position + 1,
|
||||
),
|
||||
);
|
||||
if (added != null) {
|
||||
await todo.sections.add(added);
|
||||
}
|
||||
}
|
||||
|
||||
/// Quick add into [projectId], above its sections.
|
||||
Future<void> showQuickAddIn(BuildContext context, String projectId) =>
|
||||
showQuickAdd(context, projectId: projectId);
|
||||
|
||||
/// The button adding a task, above the navigation bar (ADR 0033, D2).
|
||||
class const AddTaskButton({required final VoidCallback onPressed, super.key})
|
||||
extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) => FloatingActionButton(
|
||||
// Every tab keeps its own, alive beside the others: no hero flies.
|
||||
heroTag: null,
|
||||
tooltip: AppLocalizations.of(context).taskAddTitle,
|
||||
onPressed: onPressed,
|
||||
child: const Icon(LucideIcons.plus),
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
import 'dart:async';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_core/halkyon_core.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/label/model/label.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/task/model/task_views.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,
|
||||
/// tasks and labels, those that open; and how many rows of each kind don't.
|
||||
final class const TodoState({
|
||||
final bool loaded = false,
|
||||
final List<StoredProject> projects = const [],
|
||||
final List<StoredSection> sections = const [],
|
||||
final List<OpenTask> tasks = const [],
|
||||
final List<StoredLabel> labels = const [],
|
||||
|
||||
/// How many rows don't open on this device: another device of the
|
||||
/// person's wrote them before this one held the keys, or the instance
|
||||
/// made them up.
|
||||
final int locked = 0,
|
||||
}) with Equatable {
|
||||
/// The Inbox's ID; null before the Inbox is there.
|
||||
String? get inboxId => projects
|
||||
.where((project) => project.inbox)
|
||||
.map((project) => project.id)
|
||||
.firstOrNull;
|
||||
|
||||
/// The project [id], opened; null for one gone or locked.
|
||||
StoredProject? projectOf(String id) => projects
|
||||
.where((project) => project.id == id && project.content != null)
|
||||
.firstOrNull;
|
||||
|
||||
/// The sections of the project [projectId] that open, in their order.
|
||||
List<StoredSection> sectionsOf(String projectId) => [
|
||||
for (final section in sections)
|
||||
if (section.content?.projectId == projectId) section,
|
||||
]..sort((a, b) => a.content!.position.compareTo(b.content!.position));
|
||||
|
||||
/// The task [id]; null for one gone or locked.
|
||||
OpenTask? taskOf(String id) =>
|
||||
tasks.where((task) => task.id == id).firstOrNull;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
loaded,
|
||||
projects,
|
||||
sections,
|
||||
tasks,
|
||||
labels,
|
||||
locked,
|
||||
];
|
||||
}
|
||||
|
||||
/// Reads the person's data into [TodoState], and again whenever it changes
|
||||
/// here or a sync brings changes from their other devices.
|
||||
class TodoCubit(final TodoPerson _todo) extends Cubit<TodoState> {
|
||||
this : super(const TodoState());
|
||||
|
||||
final _subscriptions = <StreamSubscription<void>>[];
|
||||
late final _reads = CoalescingRunner(
|
||||
_read,
|
||||
onError: (error, stackTrace) =>
|
||||
log("The person's tasks not read: $error", name: 'Todo'),
|
||||
);
|
||||
|
||||
/// Reads it all, and follows every change.
|
||||
void open() {
|
||||
for (final changes in [
|
||||
_todo.projects.changes,
|
||||
_todo.sections.changes,
|
||||
_todo.tasks.changes,
|
||||
_todo.labels.changes,
|
||||
]) {
|
||||
_subscriptions.add(changes.listen((_) => unawaited(_reads.run())));
|
||||
}
|
||||
unawaited(_reads.run());
|
||||
}
|
||||
|
||||
Future<void> _read() async {
|
||||
final projects = await _todo.projects.all();
|
||||
final sections = await _todo.sections.all();
|
||||
final tasks = await _todo.tasks.all();
|
||||
final labels = await _todo.labels.all();
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
emit(
|
||||
TodoState(
|
||||
loaded: true,
|
||||
projects: projects,
|
||||
sections: [
|
||||
for (final section in sections)
|
||||
if (section.content != null) section,
|
||||
],
|
||||
tasks: openTasks(tasks),
|
||||
labels: [
|
||||
for (final label in labels)
|
||||
if (label.content != null) label,
|
||||
],
|
||||
locked: [
|
||||
...projects.map((row) => row.content),
|
||||
...sections.map((row) => row.content),
|
||||
...tasks.map((row) => row.content),
|
||||
...labels.map((row) => row.content),
|
||||
].where((content) => content == null).length,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
for (final subscription in _subscriptions) {
|
||||
await subscription.cancel();
|
||||
}
|
||||
await super.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
|
||||
/// A task that opens here: its ID and what it is.
|
||||
typedef OpenTask = ({String id, TaskContent content});
|
||||
|
||||
/// The tasks of [all] that open here, locked ones left out.
|
||||
List<OpenTask> openTasks(Iterable<StoredTask> all) => [
|
||||
for (final task in all)
|
||||
if (task.content case final content?) (id: task.id, content: content),
|
||||
];
|
||||
|
||||
/// Orders tasks as a list shows them: by their position, ties by when they
|
||||
/// were made.
|
||||
int byPosition(OpenTask a, OpenTask b) {
|
||||
final position = a.content.position.compareTo(b.content.position);
|
||||
return position != 0 ? position : a.id.compareTo(b.id);
|
||||
}
|
||||
|
||||
/// Orders tasks as a day shows them (ADR 0033, D8): those with a time first,
|
||||
/// by it, then by priority, then by their position.
|
||||
int byTime(OpenTask a, OpenTask b) {
|
||||
final (at, bt) = (a.content.date?.time, b.content.date?.time);
|
||||
if (at != bt) {
|
||||
if (at == null) {
|
||||
return 1;
|
||||
}
|
||||
if (bt == null) {
|
||||
return -1;
|
||||
}
|
||||
return at.compareTo(bt);
|
||||
}
|
||||
final priority = a.content.priority.index.compareTo(b.content.priority.index);
|
||||
return priority != 0 ? priority : byPosition(a, b);
|
||||
}
|
||||
|
||||
/// The tasks of the project [projectId] directly in it (no sub-tasks), in
|
||||
/// the section [sectionId] (none: above the sections), done ones only where
|
||||
/// [completed], in their order.
|
||||
List<OpenTask> tasksIn(
|
||||
List<OpenTask> tasks,
|
||||
String projectId, {
|
||||
String? sectionId,
|
||||
bool completed = false,
|
||||
}) => [
|
||||
for (final task in tasks)
|
||||
if (task.content.projectId == projectId &&
|
||||
task.content.parentId == null &&
|
||||
task.content.sectionId == sectionId &&
|
||||
(completed || !task.content.completed))
|
||||
task,
|
||||
]..sort(byPosition);
|
||||
|
||||
/// The sub-tasks of [parentId], in their order, done ones too.
|
||||
List<OpenTask> subTasksOf(List<OpenTask> tasks, String parentId) => [
|
||||
for (final task in tasks)
|
||||
if (task.content.parentId == parentId) task,
|
||||
]..sort(byPosition);
|
||||
|
||||
/// What Today shows on [today] (ADR 0033, D8): the `overdue` tasks not done,
|
||||
/// dated before it, oldest first; and `due` ones, dated on it, done ones too
|
||||
/// where [completed].
|
||||
({List<OpenTask> overdue, List<OpenTask> due}) todayOf(
|
||||
List<OpenTask> tasks,
|
||||
Day today, {
|
||||
bool completed = false,
|
||||
}) {
|
||||
final overdue = <OpenTask>[];
|
||||
final due = <OpenTask>[];
|
||||
for (final task in tasks) {
|
||||
final day = task.content.date?.day;
|
||||
if (day == null) {
|
||||
continue;
|
||||
}
|
||||
if (day < today && !task.content.completed) {
|
||||
overdue.add(task);
|
||||
} else if (day == today && (completed || !task.content.completed)) {
|
||||
due.add(task);
|
||||
}
|
||||
}
|
||||
overdue.sort((a, b) {
|
||||
final days = a.content.date!.day.compareTo(b.content.date!.day);
|
||||
return days != 0 ? days : byTime(a, b);
|
||||
});
|
||||
return (overdue: overdue, due: due..sort(byTime));
|
||||
}
|
||||
|
||||
/// What Upcoming shows from [today] on (ADR 0033, D8): the overdue tasks,
|
||||
/// then each of the [days] days with its tasks (none for a day without),
|
||||
/// done ones too where [completed].
|
||||
({List<OpenTask> overdue, List<(Day, List<OpenTask>)> days}) upcomingOf(
|
||||
List<OpenTask> tasks,
|
||||
Day today, {
|
||||
int days = 14,
|
||||
bool completed = false,
|
||||
}) {
|
||||
final byDay = <Day, List<OpenTask>>{
|
||||
for (var i = 0; i < days; i++) today.plus(i): [],
|
||||
};
|
||||
for (final task in tasks) {
|
||||
if (task.content.date?.day case final day?
|
||||
when byDay.containsKey(day) && (completed || !task.content.completed)) {
|
||||
byDay[day]!.add(task);
|
||||
}
|
||||
}
|
||||
return (
|
||||
overdue: todayOf(tasks, today).overdue,
|
||||
days: [
|
||||
for (final MapEntry(:key, :value) in byDay.entries)
|
||||
(key, value..sort(byTime)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// How many of [tasks] in the project [projectId] are still to be done,
|
||||
/// sub-tasks counted.
|
||||
int openCountIn(List<OpenTask> tasks, String projectId) => tasks
|
||||
.where(
|
||||
(task) => task.content.projectId == projectId && !task.content.completed,
|
||||
)
|
||||
.length;
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// A date chosen ([TaskDate] or none, to clear it); null from [pickTaskDate]
|
||||
/// where the person changed their mind.
|
||||
typedef DateChoice = ({TaskDate? date});
|
||||
|
||||
/// Asks for a task's date (ADR 0033, D8): today, tomorrow, the coming
|
||||
/// weekend, next week (the day the language starts it on), a day from the
|
||||
/// calendar, with a time where the person adds one; or none, where
|
||||
/// [current] has one.
|
||||
Future<DateChoice?> pickTaskDate(
|
||||
BuildContext context, {
|
||||
TaskDate? current,
|
||||
}) => showAdaptiveSheet<DateChoice>(
|
||||
context,
|
||||
builder: (context) => _DateSheet(current: current),
|
||||
);
|
||||
|
||||
/// Asks for a deadline, a day alone: none to clear it, null where the
|
||||
/// person changed their mind.
|
||||
Future<({Day? day})?> pickDeadline(BuildContext context, {Day? current}) async {
|
||||
final today = Day.of(clock.now());
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: (current ?? today).toDate(),
|
||||
firstDate: DateTime(today.year - 5),
|
||||
lastDate: DateTime(today.year + 20),
|
||||
);
|
||||
return picked == null ? null : (day: Day.of(picked));
|
||||
}
|
||||
|
||||
class const _DateSheet({required final TaskDate? current})
|
||||
extends StatefulWidget {
|
||||
@override
|
||||
State<_DateSheet> createState() => _DateSheetState();
|
||||
}
|
||||
|
||||
class _DateSheetState() extends State<_DateSheet> {
|
||||
late int? _time = widget.current?.time;
|
||||
late final Duration? _duration = widget.current?.duration;
|
||||
|
||||
void _choose(Day day) => Navigator.of(
|
||||
context,
|
||||
).pop<DateChoice>((date: TaskDate(day, time: _time, duration: _duration)));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final today = Day.of(clock.now());
|
||||
final firstDay = MaterialLocalizations.of(context).firstDayOfWeekIndex;
|
||||
// DateTime's weekdays run 1 (Monday) to 7, Material's 0 (Sunday) to 6.
|
||||
int until(int weekday) => (weekday - today.toDate().weekday + 7) % 7;
|
||||
final nextWeek = today.plus(
|
||||
switch (until(firstDay == 0 ? 7 : firstDay)) {
|
||||
0 => 7,
|
||||
final days => days,
|
||||
},
|
||||
);
|
||||
final weekend = today.plus(until(DateTime.saturday));
|
||||
Widget option(IconData icon, String label, Day day) => ListTile(
|
||||
leading: Icon(icon),
|
||||
title: Text(label),
|
||||
trailing: Text(dayText(context, day, today)),
|
||||
onTap: () => _choose(day),
|
||||
);
|
||||
return SheetScaffold(
|
||||
title: l10n.dateTitle,
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
option(LucideIcons.calendarCheck, l10n.dayToday, today),
|
||||
option(LucideIcons.sun, l10n.dayTomorrow, today.plus(1)),
|
||||
if (weekend != today && weekend != today.plus(1))
|
||||
option(LucideIcons.sofa, l10n.dateWeekend, weekend),
|
||||
option(LucideIcons.calendarArrowUp, l10n.dateNextWeek, nextWeek),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.calendarDays),
|
||||
title: Text(l10n.datePick),
|
||||
onTap: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: (widget.current?.day ?? today).toDate(),
|
||||
firstDate: DateTime(today.year - 5),
|
||||
lastDate: DateTime(today.year + 20),
|
||||
);
|
||||
if (picked != null && context.mounted) {
|
||||
_choose(Day.of(picked));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.clock),
|
||||
title: Text(l10n.dateTime),
|
||||
trailing: switch (_time) {
|
||||
final time? => InputChip(
|
||||
label: Text(timeText(context, time)),
|
||||
onDeleted: () => setState(() => _time = null),
|
||||
),
|
||||
null => null,
|
||||
},
|
||||
onTap: () async {
|
||||
final picked = await showTimePicker(
|
||||
context: context,
|
||||
initialTime: switch (_time) {
|
||||
final time? => TimeOfDay(
|
||||
hour: time ~/ 60,
|
||||
minute: time % 60,
|
||||
),
|
||||
null => const TimeOfDay(hour: 9, minute: 0),
|
||||
},
|
||||
);
|
||||
if (picked != null) {
|
||||
setState(() => _time = picked.hour * 60 + picked.minute);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.current != null)
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.calendarX),
|
||||
title: Text(l10n.dateNone),
|
||||
onTap: () => Navigator.of(context).pop<DateChoice>((date: null)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/date_picker.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_pickers.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Quick add (ADR 0033, D8): a task's title, and chips for its date,
|
||||
/// priority, project and labels, and a description where asked for. It
|
||||
/// goes into [projectId] (the Inbox where none is given), [sectionId] and
|
||||
/// under [parentId] where given, dated [day] where given, last among its
|
||||
/// siblings. The sheet stays for the next task; going back closes it.
|
||||
Future<void> showQuickAdd(
|
||||
BuildContext context, {
|
||||
String? projectId,
|
||||
String? sectionId,
|
||||
String? parentId,
|
||||
Day? day,
|
||||
}) => showAdaptiveSheet<void>(
|
||||
context,
|
||||
builder: (context) => _QuickAdd(
|
||||
projectId: projectId ?? context.read<TodoCubit>().state.inboxId,
|
||||
sectionId: sectionId,
|
||||
parentId: parentId,
|
||||
day: day,
|
||||
),
|
||||
);
|
||||
|
||||
class const _QuickAdd({
|
||||
required final String? projectId,
|
||||
required final String? sectionId,
|
||||
required final String? parentId,
|
||||
required final Day? day,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<_QuickAdd> createState() => _QuickAddState();
|
||||
}
|
||||
|
||||
class _QuickAddState() extends State<_QuickAdd> {
|
||||
final _title = TextEditingController();
|
||||
final _description = TextEditingController();
|
||||
final _focus = FocusNode();
|
||||
late TaskDate? _date = switch (widget.day) {
|
||||
final day? => TaskDate(day),
|
||||
null => null,
|
||||
};
|
||||
var _priority = Priority.none;
|
||||
late String? _projectId = widget.projectId;
|
||||
late String? _sectionId = widget.sectionId;
|
||||
var _labelIds = <String>[];
|
||||
var _describing = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_title.dispose();
|
||||
_description.dispose();
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _add() async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final title = _title.text.trim();
|
||||
final projectId = _projectId;
|
||||
if (title.isEmpty || projectId == null) {
|
||||
return;
|
||||
}
|
||||
final state = context.read<TodoCubit>().state;
|
||||
final siblings = state.tasks.where(
|
||||
(task) =>
|
||||
task.content.projectId == projectId &&
|
||||
task.content.sectionId == _sectionId &&
|
||||
task.content.parentId == widget.parentId,
|
||||
);
|
||||
final last = siblings.fold<double>(
|
||||
0,
|
||||
(position, task) =>
|
||||
task.content.position > position ? task.content.position : position,
|
||||
);
|
||||
try {
|
||||
await PersonScope.of<TodoPerson>(context).tasks.add(
|
||||
TaskContent(
|
||||
title: title,
|
||||
projectId: projectId,
|
||||
sectionId: _sectionId,
|
||||
parentId: widget.parentId,
|
||||
description: _description.text.trim(),
|
||||
position: siblings.isEmpty ? 0 : last + 1,
|
||||
date: _date,
|
||||
priority: _priority,
|
||||
labelIds: _labelIds,
|
||||
),
|
||||
);
|
||||
} on KeysMissing {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.keysMissing)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
// Ready for the next one, in the same place and on the same day.
|
||||
_title.clear();
|
||||
_description.clear();
|
||||
setState(() {
|
||||
_priority = Priority.none;
|
||||
_labelIds = [];
|
||||
});
|
||||
_focus.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final state = context.watch<TodoCubit>().state;
|
||||
final today = Day.of(clock.now());
|
||||
final project = switch (_projectId) {
|
||||
final id? => state.projectOf(id),
|
||||
null => null,
|
||||
};
|
||||
return SheetScaffold(
|
||||
title: widget.parentId == null ? l10n.taskAddTitle : l10n.subTaskAdd,
|
||||
hasSnackBars: true,
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _title,
|
||||
focusNode: _focus,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.done,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(hintText: l10n.taskTitleHint),
|
||||
onSubmitted: (_) => _add(),
|
||||
),
|
||||
if (_describing)
|
||||
TextField(
|
||||
controller: _description,
|
||||
minLines: 1,
|
||||
maxLines: 4,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(hintText: l10n.taskDescriptionHint),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
LucideIcons.calendar,
|
||||
color: switch (_date) {
|
||||
final date? => dateColor(context, date.day, today),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
label: Text(switch (_date) {
|
||||
final date? => dateText(context, date, today),
|
||||
null => l10n.dateTitle,
|
||||
}),
|
||||
onPressed: () async {
|
||||
if (await pickTaskDate(context, current: _date) case (
|
||||
:final date,
|
||||
)) {
|
||||
setState(() => _date = date);
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
LucideIcons.flag,
|
||||
color: priorityColor(context, _priority),
|
||||
),
|
||||
label: Text(
|
||||
_priority == Priority.none
|
||||
? l10n.priorityTitle
|
||||
: priorityText(l10n, _priority),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (await pickPriority(context, _priority)
|
||||
case final priority?) {
|
||||
setState(() => _priority = priority);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (widget.parentId == null)
|
||||
ActionChip(
|
||||
avatar: Icon(
|
||||
project?.inbox ?? true
|
||||
? LucideIcons.inbox
|
||||
: LucideIcons.hash,
|
||||
),
|
||||
label: Text(switch (project) {
|
||||
final project? when !project.inbox => project.content!.name,
|
||||
_ => l10n.shellTabInbox,
|
||||
}),
|
||||
onPressed: () async {
|
||||
if (await pickProject(context, _projectId) case final id?) {
|
||||
setState(() {
|
||||
if (id != _projectId) {
|
||||
_sectionId = null;
|
||||
}
|
||||
_projectId = id;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionChip(
|
||||
avatar: const Icon(LucideIcons.tag),
|
||||
label: Text(
|
||||
_labelIds.isEmpty
|
||||
? l10n.labelsTitle
|
||||
: l10n.labelsCount(_labelIds.length),
|
||||
),
|
||||
onPressed: () async {
|
||||
if (await pickLabels(context, _labelIds) case final labels?) {
|
||||
setState(() => _labelIds = labels);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!_describing)
|
||||
ActionChip(
|
||||
avatar: const Icon(LucideIcons.text),
|
||||
label: Text(l10n.taskDescription),
|
||||
onPressed: () => setState(() => _describing = true),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
footer: ListenableBuilder(
|
||||
listenable: _title,
|
||||
builder: (context, _) => FilledButton.icon(
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(l10n.taskAdd),
|
||||
onPressed: _title.text.trim().isEmpty ? null : _add,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Asks for a priority, [current] ticked; null where the person changed
|
||||
/// their mind.
|
||||
Future<Priority?> pickPriority(BuildContext context, Priority current) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
return showDialog<Priority>(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: Text(l10n.priorityTitle),
|
||||
children: [
|
||||
for (final priority in Priority.values)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
LucideIcons.flag,
|
||||
color: priorityColor(context, priority),
|
||||
),
|
||||
title: Text(priorityText(l10n, priority)),
|
||||
trailing: priority == current
|
||||
? const Icon(LucideIcons.check)
|
||||
: null,
|
||||
onTap: () => Navigator.of(context).pop(priority),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Asks for a project, the Inbox first, [current] ticked; its ID, or null
|
||||
/// where the person changed their mind.
|
||||
Future<String?> pickProject(BuildContext context, String? current) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final state = context.read<TodoCubit>().state;
|
||||
return showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => SimpleDialog(
|
||||
title: Text(l10n.projectPickTitle),
|
||||
children: [
|
||||
for (final project in state.projects)
|
||||
if (project.content case final content?)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
project.inbox ? LucideIcons.inbox : LucideIcons.hash,
|
||||
color: switch (content.color) {
|
||||
final color? => paletteText(context, color),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
title: Text(project.inbox ? l10n.shellTabInbox : content.name),
|
||||
trailing: project.id == current
|
||||
? const Icon(LucideIcons.check)
|
||||
: null,
|
||||
onTap: () => Navigator.of(context).pop(project.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Asks which labels a task has, [current] ticked; null where the person
|
||||
/// changed their mind.
|
||||
Future<List<String>?> pickLabels(BuildContext context, List<String> current) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final labels = context.read<TodoCubit>().state.labels;
|
||||
return showDialog<List<String>>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final chosen = {...current};
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) => AlertDialog(
|
||||
title: Text(l10n.labelsTitle),
|
||||
content: labels.isEmpty
|
||||
? Text(l10n.labelsNone)
|
||||
: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final label in labels)
|
||||
CheckboxListTile(
|
||||
value: chosen.contains(label.id),
|
||||
title: Text(label.content!.name),
|
||||
onChanged: (on) => setState(
|
||||
() => on ?? false
|
||||
? chosen.add(label.id)
|
||||
: chosen.remove(label.id),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop([
|
||||
for (final label in labels)
|
||||
if (chosen.contains(label.id)) label.id,
|
||||
]),
|
||||
child: Text(MaterialLocalizations.of(context).okButtonLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,321 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:clock/clock.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:halkyon_app/halkyon_app.dart';
|
||||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/app/todo_dependencies.dart';
|
||||
import 'package:halkyon_todo_flutter/src/core/privacy/todo_keys.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/bloc/todo_cubit.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/data/task_repository.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task_views.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/date_picker.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/quick_add_sheet.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_pickers.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_texts.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/widget/task_tile.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Opens the task [id]'s sheet (ADR 0033, D8): its title and description to
|
||||
/// edit, a row for each of its date, deadline, priority, project and labels,
|
||||
/// its sub-tasks with one more to add, and deleting it, which asks first.
|
||||
Future<void> openTaskSheet(BuildContext context, String id) =>
|
||||
showAdaptiveSheet<void>(context, builder: (context) => _TaskSheet(id: id));
|
||||
|
||||
class const _TaskSheet({required final String id}) extends StatefulWidget {
|
||||
@override
|
||||
State<_TaskSheet> createState() => _TaskSheetState();
|
||||
}
|
||||
|
||||
class _TaskSheetState() extends State<_TaskSheet> {
|
||||
late final TextEditingController _title;
|
||||
late final TextEditingController _description;
|
||||
late final TaskRepository _tasks;
|
||||
|
||||
/// Both read in [initState], as [dispose] can't look them up any more.
|
||||
late final TodoCubit _todo;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_todo = context.read<TodoCubit>();
|
||||
_tasks = PersonScope.of<TodoPerson>(context).tasks;
|
||||
final content = _todo.state.taskOf(widget.id)?.content;
|
||||
_title = TextEditingController(text: content?.title);
|
||||
_description = TextEditingController(text: content?.description);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// What was typed is kept as the sheet closes.
|
||||
unawaited(_saveText());
|
||||
_title.dispose();
|
||||
_description.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
TaskContent? get _content => _todo.state.taskOf(widget.id)?.content;
|
||||
|
||||
Future<void> _change(TaskContent Function(TaskContent content) change) async {
|
||||
final content = _content;
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _tasks.update(widget.id, change(content));
|
||||
} on KeysMissing {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context).keysMissing)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveText() async {
|
||||
final content = _content;
|
||||
final title = _title.text.trim();
|
||||
final description = _description.text.trim();
|
||||
if (content == null ||
|
||||
title.isEmpty ||
|
||||
(title == content.title && description == content.description)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _tasks.update(
|
||||
widget.id,
|
||||
content.copyWith(title: title, description: description),
|
||||
);
|
||||
} on KeysMissing {
|
||||
// Nothing is written on a device without the keys.
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the task, with its sub-tasks, to the project [projectId].
|
||||
Future<void> _move(String projectId) async {
|
||||
final state = context.read<TodoCubit>().state;
|
||||
await _change(
|
||||
(content) =>
|
||||
content.copyWith(projectId: projectId, sectionId: () => null),
|
||||
);
|
||||
final below = <OpenTask>[];
|
||||
void collect(String id) {
|
||||
for (final task in subTasksOf(state.tasks, id)) {
|
||||
below.add(task);
|
||||
collect(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
collect(widget.id);
|
||||
for (final task in below) {
|
||||
await _tasks.update(
|
||||
task.id,
|
||||
task.content.copyWith(projectId: projectId, sectionId: () => null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final state = context.watch<TodoCubit>().state;
|
||||
final task = state.taskOf(widget.id);
|
||||
if (task == null) {
|
||||
// Deleted here or on another device.
|
||||
return SheetScaffold(
|
||||
title: l10n.taskTitle,
|
||||
body: Text(l10n.taskGone),
|
||||
);
|
||||
}
|
||||
final content = task.content;
|
||||
final today = Day.of(clock.now());
|
||||
final project = state.projectOf(content.projectId);
|
||||
final subTasks = subTasksOf(state.tasks, task.id);
|
||||
final labels = {
|
||||
for (final label in state.labels) label.id: label.content!.name,
|
||||
};
|
||||
final muted = theme.colorScheme.onSurfaceVariant;
|
||||
Widget clear(VoidCallback onPressed) => IconButton(
|
||||
tooltip: l10n.clear,
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
return SheetScaffold(
|
||||
title: switch (project) {
|
||||
final project? when !project.inbox => project.content!.name,
|
||||
_ => l10n.shellTabInbox,
|
||||
},
|
||||
hasSnackBars: true,
|
||||
action: IconButton(
|
||||
tooltip: l10n.taskDelete,
|
||||
icon: const Icon(LucideIcons.trash2),
|
||||
onPressed: () async {
|
||||
final sheet = Navigator.of(context);
|
||||
if (await confirmDestructiveAction(
|
||||
context,
|
||||
title: l10n.taskDeleteTitle,
|
||||
message: subTasks.isEmpty
|
||||
? l10n.taskDeleteMessage(content.title)
|
||||
: l10n.taskDeleteWithSubTasks(content.title, subTasks.length),
|
||||
confirmLabel: l10n.taskDelete,
|
||||
)) {
|
||||
await _tasks.delete(widget.id);
|
||||
sheet.pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
body: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
TaskCheck(task: task),
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _title,
|
||||
style: theme.textTheme.titleLarge,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: l10n.taskTitleHint,
|
||||
),
|
||||
onSubmitted: (_) => _saveText(),
|
||||
onTapOutside: (_) => unawaited(_saveText()),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
TextField(
|
||||
controller: _description,
|
||||
minLines: 1,
|
||||
maxLines: 8,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
prefixIcon: const Icon(LucideIcons.text),
|
||||
hintText: l10n.taskDescriptionHint,
|
||||
),
|
||||
onTapOutside: (_) => unawaited(_saveText()),
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
LucideIcons.calendar,
|
||||
color: switch (content.date) {
|
||||
final date? => dateColor(context, date.day, today),
|
||||
null => null,
|
||||
},
|
||||
),
|
||||
title: Text(switch (content.date) {
|
||||
final date? => dateText(context, date, today),
|
||||
null => l10n.dateNoneYet,
|
||||
}),
|
||||
trailing: content.date == null
|
||||
? null
|
||||
: clear(() => _change((c) => c.copyWith(date: () => null))),
|
||||
onTap: () async {
|
||||
if (await pickTaskDate(context, current: content.date) case (
|
||||
:final date,
|
||||
)) {
|
||||
await _change((c) => c.copyWith(date: () => date));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.target),
|
||||
title: Text(switch (content.deadline) {
|
||||
final day? => l10n.deadlineOn(dayText(context, day, today)),
|
||||
null => l10n.deadlineNone,
|
||||
}),
|
||||
trailing: content.deadline == null
|
||||
? null
|
||||
: clear(() => _change((c) => c.copyWith(deadline: () => null))),
|
||||
onTap: () async {
|
||||
if (await pickDeadline(context, current: content.deadline) case (
|
||||
:final day,
|
||||
)) {
|
||||
await _change((c) => c.copyWith(deadline: () => day));
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
LucideIcons.flag,
|
||||
color: priorityColor(context, content.priority),
|
||||
),
|
||||
title: Text(priorityText(l10n, content.priority)),
|
||||
onTap: () async {
|
||||
if (await pickPriority(context, content.priority)
|
||||
case final priority?) {
|
||||
await _change((c) => c.copyWith(priority: priority));
|
||||
}
|
||||
},
|
||||
),
|
||||
if (content.parentId == null)
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
project?.inbox ?? true ? LucideIcons.inbox : LucideIcons.hash,
|
||||
),
|
||||
title: Text(switch (project) {
|
||||
final project? when !project.inbox => project.content!.name,
|
||||
_ => l10n.shellTabInbox,
|
||||
}),
|
||||
onTap: () async {
|
||||
if (await pickProject(context, content.projectId) case final id?
|
||||
when id != content.projectId) {
|
||||
await _move(id);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(LucideIcons.tag),
|
||||
title: Text(
|
||||
content.labelIds.isEmpty
|
||||
? l10n.labelsNoneSet
|
||||
: [
|
||||
for (final id in content.labelIds) ?labels[id],
|
||||
].join(', '),
|
||||
),
|
||||
onTap: () async {
|
||||
if (await pickLabels(context, content.labelIds)
|
||||
case final chosen?) {
|
||||
await _change((c) => c.copyWith(labelIds: chosen));
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(
|
||||
subTasks.isEmpty
|
||||
? l10n.subTasks
|
||||
: l10n.subTasksDone(
|
||||
subTasks.where((t) => t.content.completed).length,
|
||||
subTasks.length,
|
||||
),
|
||||
style: theme.textTheme.titleSmall?.copyWith(color: muted),
|
||||
),
|
||||
),
|
||||
for (final sub in subTasks) TaskTile(task: sub),
|
||||
Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: TextButton.icon(
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(l10n.subTaskAdd),
|
||||
onPressed: () => showQuickAdd(
|
||||
context,
|
||||
projectId: content.projectId,
|
||||
sectionId: content.sectionId,
|
||||
parentId: task.id,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import 'package:halkyon_todo_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_todo_flutter/src/feature/task/model/task.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// [minutes] after midnight as the language writes a time of day.
|
||||
String timeText(BuildContext context, int minutes) => DateFormat.Hm(
|
||||
Localizations.localeOf(context).toString(),
|
||||
).format(DateTime(2000, 1, 1, minutes ~/ 60, minutes % 60));
|
||||
|
||||
/// [day] as a task's row names it, seen on [today]: Today, Tomorrow,
|
||||
/// Yesterday, the weekday within the coming week, else the day and month,
|
||||
/// and the year where it isn't this one.
|
||||
String dayText(BuildContext context, Day day, Day today) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final locale = Localizations.localeOf(context).toString();
|
||||
final days = today.daysUntil(day);
|
||||
return switch (days) {
|
||||
0 => l10n.dayToday,
|
||||
1 => l10n.dayTomorrow,
|
||||
-1 => l10n.dayYesterday,
|
||||
> 1 && < 7 => DateFormat.EEEE(locale).format(day.toDate()),
|
||||
_ when day.year == today.year => DateFormat.MMMd(
|
||||
locale,
|
||||
).format(day.toDate()),
|
||||
_ => DateFormat.yMMMd(locale).format(day.toDate()),
|
||||
};
|
||||
}
|
||||
|
||||
/// [date] as a task's row names it: its day, and its time or its time span.
|
||||
String dateText(BuildContext context, TaskDate date, Day today) {
|
||||
final day = dayText(context, date.day, today);
|
||||
final time = date.time;
|
||||
if (time == null) {
|
||||
return day;
|
||||
}
|
||||
final start = timeText(context, time);
|
||||
return switch (date.duration) {
|
||||
final duration? =>
|
||||
'$day $start–${timeText(context, (time + duration.inMinutes) % (24 * 60))}',
|
||||
null => '$day $start',
|
||||
};
|
||||
}
|
||||
|
||||
/// [color] for text and icons on the screen's surface: its darker outline
|
||||
/// in light mode, its fill in dark mode, so it reads on both.
|
||||
Color paletteText(BuildContext context, PaletteColor color) =>
|
||||
Theme.of(context).brightness == Brightness.light
|
||||
? color.outline
|
||||
: color.background;
|
||||
|
||||
/// The colour a task's date shows in (ADR 0033, D9): cerise overdue, fern
|
||||
/// today, carrot tomorrow, purple in the coming week, the muted text colour
|
||||
/// later, and muted for a task done.
|
||||
Color dateColor(BuildContext context, Day day, Day today, {bool done = false}) {
|
||||
final days = today.daysUntil(day);
|
||||
final color = switch (days) {
|
||||
_ when done => null,
|
||||
< 0 => PaletteColor.cerise,
|
||||
0 => PaletteColor.fern,
|
||||
1 => PaletteColor.carrot,
|
||||
< 7 => PaletteColor.purple,
|
||||
_ => null,
|
||||
};
|
||||
return color == null
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: paletteText(context, color);
|
||||
}
|
||||
|
||||
/// The colour of a task's priority (ADR 0033, D9): cerise, carrot, cobalt;
|
||||
/// none for one not marked.
|
||||
Color? priorityColor(BuildContext context, Priority priority) =>
|
||||
switch (priority) {
|
||||
Priority.p1 => paletteText(context, PaletteColor.cerise),
|
||||
Priority.p2 => paletteText(context, PaletteColor.carrot),
|
||||
Priority.p3 => paletteText(context, PaletteColor.slateblue),
|
||||
Priority.none => null,
|
||||
};
|
||||
|
||||
/// The name of [priority], as its menu lists it.
|
||||
String priorityText(AppLocalizations l10n, Priority priority) =>
|
||||
switch (priority) {
|
||||
Priority.p1 => l10n.priority1,
|
||||
Priority.p2 => l10n.priority2,
|
||||
Priority.p3 => l10n.priority3,
|
||||
Priority.none => l10n.priority4,
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue