feat(todo): Upcoming's tasks dragged onto another day (ADR 0033, D16)
A row of Upcoming, overdue ones too, held and dropped on another day moves there, its time, duration and recurrence kept; Moved to … offers Undo. The tint over a day a drag hovers is laid beside the rows, not around them, which paint on the Material beneath. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
7fe74ed0d5
commit
c83f7c46ff
8 changed files with 173 additions and 15 deletions
|
|
@ -167,5 +167,6 @@
|
|||
"quickAddTokenHint": "Tippe ein hervorgehobenes Wort an, um es als Text zu behalten.",
|
||||
"deadlineTitle": "Frist",
|
||||
"orderChanged": "Reihenfolge geändert",
|
||||
"addTaskCancel": "Hier ablegen zum Abbrechen"
|
||||
"addTaskCancel": "Hier ablegen zum Abbrechen",
|
||||
"taskMovedTo": "Verschoben auf {day}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -457,5 +457,14 @@
|
|||
"addTaskCancel": "Drop here to cancel",
|
||||
"@addTaskCancel": {
|
||||
"description": "The × left in the add button's place while it is dragged (D16)."
|
||||
},
|
||||
"taskMovedTo": "Moved to {day}",
|
||||
"@taskMovedTo": {
|
||||
"description": "Snack bar after a task was dragged onto another day in Upcoming (ADR 0033, D16).",
|
||||
"placeholders": {
|
||||
"day": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1107,6 +1107,12 @@ abstract class AppLocalizations {
|
|||
/// In en, this message translates to:
|
||||
/// **'Drop here to cancel'**
|
||||
String get addTaskCancel;
|
||||
|
||||
/// Snack bar after a task was dragged onto another day in Upcoming (ADR 0033, D16).
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Moved to {day}'**
|
||||
String taskMovedTo(String day);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
|
|
|||
|
|
@ -713,4 +713,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get addTaskCancel => 'Hier ablegen zum Abbrechen';
|
||||
|
||||
@override
|
||||
String taskMovedTo(String day) {
|
||||
return 'Verschoben auf $day';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -712,6 +712,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
|
||||
@override
|
||||
String get addTaskCancel => 'Drop here to cancel';
|
||||
|
||||
@override
|
||||
String taskMovedTo(String day) {
|
||||
return 'Moved to $day';
|
||||
}
|
||||
}
|
||||
|
||||
/// The translations for English, as used in the United Kingdom (`en_GB`).
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
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/model/task.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_selection.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';
|
||||
|
|
@ -264,6 +270,76 @@ class _DraggableTaskState() extends State<DraggableTask> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A day of Upcoming (D16): the add button dropped on it adds a task for
|
||||
/// [day]; a task's row moves there, its time, duration and recurrence
|
||||
/// kept, which can be undone for a moment. Tinted while either is over it.
|
||||
class const DayDropArea({
|
||||
required final Day day,
|
||||
required final Widget child,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
Future<void> _move(BuildContext context, String id) async {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final tasks = PersonScope.of<TodoPerson>(context).tasks;
|
||||
final task = context
|
||||
.read<TodoCubit>()
|
||||
.state
|
||||
.tasks
|
||||
.where((task) => task.id == id)
|
||||
.firstOrNull;
|
||||
if (task == null || task.content.date?.day == day) {
|
||||
return;
|
||||
}
|
||||
final content = task.content;
|
||||
final moved = content.copyWith(
|
||||
date: () => content.date?.on(day) ?? TaskDate(day),
|
||||
);
|
||||
try {
|
||||
await tasks.update(id, moved);
|
||||
} on KeysMissing {
|
||||
messenger.showSnackBar(SnackBar(content: Text(l10n.keysMissing)));
|
||||
return;
|
||||
}
|
||||
if (context.mounted) {
|
||||
showUndoSnackBar(
|
||||
context,
|
||||
message: l10n.taskMovedTo(
|
||||
dayText(context, day, Day.of(clock.now())),
|
||||
),
|
||||
onUndo: () => unawaited(tasks.update(id, content)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => DragTarget<Object>(
|
||||
onWillAcceptWithDetails: (details) =>
|
||||
details.data is NewTaskDrag || details.data is TaskDrag,
|
||||
onAcceptWithDetails: (details) => switch (details.data) {
|
||||
TaskDrag(:final id) => unawaited(_move(context, id)),
|
||||
_ => unawaited(showQuickAdd(context, day: day)),
|
||||
},
|
||||
// Laid over the rows beside them, not around them: the rows paint on
|
||||
// the Material beneath.
|
||||
builder: (context, candidates, _) => Stack(
|
||||
children: [
|
||||
child,
|
||||
if (candidates.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ColoredBox(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// A list's area the add button can be dropped on (D16), [onDrop] opening
|
||||
/// quick add for it; tinted while the button is over it.
|
||||
class const NewTaskDropArea({
|
||||
|
|
@ -274,11 +350,22 @@ class const NewTaskDropArea({
|
|||
@override
|
||||
Widget build(BuildContext context) => DragTarget<NewTaskDrag>(
|
||||
onAcceptWithDetails: (_) => onDrop(),
|
||||
builder: (context, candidates, _) => ColoredBox(
|
||||
color: candidates.isEmpty
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.primary.withValues(alpha: 0.08),
|
||||
child: child,
|
||||
// Laid over the rows beside them, not around them: the rows paint on
|
||||
// the Material beneath.
|
||||
builder: (context, candidates, _) => Stack(
|
||||
children: [
|
||||
child,
|
||||
if (candidates.isNotEmpty)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: ColoredBox(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -158,14 +158,18 @@ class _UpcomingTabState() extends State<UpcomingTab> {
|
|||
),
|
||||
),
|
||||
for (final task in overdue)
|
||||
TaskTile(task: task, showProject: true),
|
||||
DraggableTask(
|
||||
task: task,
|
||||
child: TaskTile(task: task, showProject: true),
|
||||
),
|
||||
],
|
||||
// The add button dropped on a day adds a task
|
||||
// for it (D16).
|
||||
// for it, a row held and dropped there moves to
|
||||
// it (D16).
|
||||
for (final (day, tasks) in days)
|
||||
NewTaskDropArea(
|
||||
DayDropArea(
|
||||
key: _keyOf(day),
|
||||
onDrop: () => showQuickAdd(context, day: day),
|
||||
day: day,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
|
|
@ -182,7 +186,13 @@ class _UpcomingTabState() extends State<UpcomingTab> {
|
|||
),
|
||||
),
|
||||
for (final task in tasks)
|
||||
TaskTile(task: task, showProject: true),
|
||||
DraggableTask(
|
||||
task: task,
|
||||
child: TaskTile(
|
||||
task: task,
|
||||
showProject: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1374,6 +1374,40 @@ void main() {
|
|||
expect(find.byKey(quickAddTitleKey), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a row of Upcoming held and dropped on another day moves '
|
||||
'there, its time kept, which can be undone', (tester) async {
|
||||
final today = Day.of(DateTime.now());
|
||||
await start(tester, usedBefore: true);
|
||||
await seed(
|
||||
tester,
|
||||
TaskContent(
|
||||
title: 'Water plants',
|
||||
projectId: inbox(),
|
||||
date: TaskDate(today, time: 23 * 60 + 30),
|
||||
),
|
||||
);
|
||||
await goTab(tester, 'Upcoming');
|
||||
final day = find.byTooltip(RegExp('^Add a task on .*Tomorrow'));
|
||||
|
||||
await dragFrom(
|
||||
tester,
|
||||
tester.getCenter(row('Water plants')),
|
||||
tester.getCenter(day) - const Offset(200, 0),
|
||||
);
|
||||
|
||||
expect(
|
||||
(await stored(tester))['Water plants']!.date,
|
||||
TaskDate(today.plus(1), time: 23 * 60 + 30),
|
||||
);
|
||||
expect(find.textContaining('Moved to'), findsOneWidget);
|
||||
await tester.tap(find.text('Undo'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
(await stored(tester))['Water plants']!.date,
|
||||
TaskDate(today, time: 23 * 60 + 30),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('the add button dropped on a day in Upcoming adds a task '
|
||||
'for that day', (tester) async {
|
||||
final tomorrow = Day.of(DateTime.now()).plus(1);
|
||||
|
|
|
|||
Loading…
Reference in a new issue