From 5d387145c8f95607bccf9050ebdd889258725c35 Mon Sep 17 00:00:00 2001 From: krille-chan Date: Sun, 29 Oct 2023 07:55:00 +0100 Subject: [PATCH] chore: Follow up todo list feature --- lib/pages/tasks/tasks.dart | 61 ++++-- lib/pages/tasks/tasks_view.dart | 363 +++++++++++++++++--------------- 2 files changed, 233 insertions(+), 191 deletions(-) diff --git a/lib/pages/tasks/tasks.dart b/lib/pages/tasks/tasks.dart index 6874a950..7743b99e 100644 --- a/lib/pages/tasks/tasks.dart +++ b/lib/pages/tasks/tasks.dart @@ -99,18 +99,19 @@ class TasksController extends State { update: (todos) => todos..removeWhere((t) => t.done), ); - void onReorder(int oldindex, int newindex) => updateTodos( - update: (todos) { - if (newindex > oldindex) { - newindex -= 1; - } - final todo = todos.removeAt(oldindex); - todos.insert(newindex, todo); - - return todos; - }, - tmpTodo: true, - ); + void onReorder(int oldindex, int newindex) { + if (newindex > oldindex) { + newindex -= 1; + } + updateTodos( + update: (todos) { + final todo = todos.removeAt(oldindex); + todos.insert(newindex, todo); + return todos; + }, + tmpTodo: true, + ); + } void updateTodos({ required List Function(List) update, @@ -122,6 +123,7 @@ class TasksController extends State { }); try { final newTodos = update(todos); + assert(todos != newTodos); if (tmpTodo) { setState(() { _tmpTodos = newTodos; @@ -130,17 +132,23 @@ class TasksController extends State { _tmpTodos = null; }); } - await widget.room.updateMatrixTodos(newTodos); + await widget.room + .updateMatrixTodos(newTodos) + .timeout(const Duration(seconds: 30)); onSuccess?.call(); } on MatrixException catch (e) { - if (e.error != MatrixError.M_LIMIT_EXCEEDED) rethrow; - Logs().w('Rate limit! Try again in ${e.raw['retry_after_ms']}ms'); - await Future.delayed( - Duration(milliseconds: e.raw['retry_after_ms'] as int), - ); + final retryAfterMs = e.retryAfterMs; + if (retryAfterMs == null) rethrow; + Logs().w('Rate limit! Try again in $retryAfterMs ms'); + await Future.delayed(Duration(milliseconds: retryAfterMs)); updateTodos(update: update, onSuccess: onSuccess); } catch (e, s) { - Logs().w('Unable to toggle done', e, s); + Logs().w('Unable to update todo list', e, s); + if (_tmpTodos != null) { + setState(() { + _tmpTodos = null; + }); + } ScaffoldMessenger.of(context).showSnackBar( SnackBar( duration: const Duration(seconds: 20), @@ -151,7 +159,13 @@ class TasksController extends State { color: Theme.of(context).colorScheme.background, ), const SizedBox(width: 16), - Text(e.toLocalizedString(context)), + Expanded( + child: Text( + e.toLocalizedString(context), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ], ), action: e is TodoListChangedException @@ -207,6 +221,13 @@ class TasksController extends State { ); } + void deleteTodo(int i) => updateTodos( + update: (list) { + list.removeAt(i); + return list; + }, + ); + void editTodoDueDate(int i, MatrixTodo todo) async { final now = DateTime.now(); final date = await showDatePicker( diff --git a/lib/pages/tasks/tasks_view.dart b/lib/pages/tasks/tasks_view.dart index d60ac291..2651572c 100644 --- a/lib/pages/tasks/tasks_view.dart +++ b/lib/pages/tasks/tasks_view.dart @@ -13,187 +13,208 @@ class TasksView extends StatelessWidget { @override Widget build(BuildContext context) { final tag = Localizations.maybeLocaleOf(context)?.toLanguageTag(); - return Scaffold( - appBar: AppBar( - title: Text(L10n.of(context)!.todoLists), - actions: [ - AnimatedCrossFade( - duration: FluffyThemes.animationDuration, - firstChild: const SizedBox( - width: 32, - height: 32, - ), - secondChild: const Padding( - padding: EdgeInsets.all(8.0), - child: SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), + return StreamBuilder( + stream: controller.widget.room.onUpdate.stream, + builder: (context, snapshot) { + final list = controller.todos; + return Scaffold( + appBar: AppBar( + title: Text(L10n.of(context)!.todoLists), + actions: [ + AnimatedCrossFade( + duration: FluffyThemes.animationDuration, + firstChild: const SizedBox( + width: 32, + height: 32, + ), + secondChild: const Padding( + padding: EdgeInsets.all(8.0), + child: SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ), + ), + crossFadeState: controller.isLoading + ? CrossFadeState.showSecond + : CrossFadeState.showFirst, ), - ), - crossFadeState: controller.isLoading - ? CrossFadeState.showSecond - : CrossFadeState.showFirst, + if (list.any((todo) => todo.done)) + IconButton( + icon: const Icon(Icons.cleaning_services_outlined), + onPressed: controller.cleanUp, + ), + ], ), - IconButton( - icon: const Icon(Icons.cleaning_services_outlined), - onPressed: controller.cleanUp, - ), - ], - ), - body: Column( - children: [ - Expanded( - child: Opacity( - opacity: controller.isLoading ? 0.66 : 1, - child: StreamBuilder( - stream: controller.widget.room.onUpdate.stream, - builder: (context, snapshot) { - final list = controller.todos; - if (list.isEmpty) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon( - Icons.task_alt, - size: 80, - color: Theme.of(context).colorScheme.secondary, - ), - const SizedBox(height: 16), - SizedBox( - width: 256, - child: Text( - L10n.of(context)!.noTodosYet, - textAlign: TextAlign.center, - ), - ), - ], - ); - } - return ReorderableListView.builder( - onReorder: controller.onReorder, - itemCount: list.length, - itemBuilder: (context, i) { - final todo = list[i]; - final description = todo.description; - final dueDate = todo.dueDate; - return ListTile( - key: Key(todo.toJson().toString()), - leading: Icon( - todo.done - ? Icons.check_circle - : Icons.circle_outlined, - ), - title: Text( - todo.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - decoration: - todo.done ? TextDecoration.lineThrough : null, - ), - ), - subtitle: description == null && dueDate == null - ? null - : Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (description != null) - Text( - description, - maxLines: 2, - ), - if (dueDate != null) - SizedBox( - height: 24, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 6, + body: Column( + children: [ + Expanded( + child: Opacity( + opacity: controller.isLoading ? 0.66 : 1, + child: list.isEmpty + ? Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + Icons.task_alt, + size: 80, + color: Theme.of(context).colorScheme.secondary, + ), + const SizedBox(height: 16), + SizedBox( + width: 256, + child: Text( + L10n.of(context)!.noTodosYet, + textAlign: TextAlign.center, + ), + ), + ], + ) + : ReorderableListView.builder( + onReorder: controller.onReorder, + itemCount: list.length, + itemBuilder: (context, i) { + final todo = list[i]; + final description = todo.description; + final dueDate = todo.dueDate; + return ListTile( + key: Key(todo.toJson().toString()), + leading: Icon( + todo.done + ? Icons.check_circle + : Icons.circle_outlined, + ), + title: Text( + todo.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + decoration: todo.done + ? TextDecoration.lineThrough + : null, + ), + ), + subtitle: description == null && dueDate == null + ? null + : Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + if (description != null) + Text( + description, + maxLines: 2, ), - ), - icon: const Icon( - Icons.calendar_month, - size: 16, - ), - label: Text( - DateFormat.yMMMd(tag).format(dueDate), - style: const TextStyle(fontSize: 12), - ), - onPressed: () => - controller.editTodoDueDate( - i, - todo, - ), - ), + if (dueDate != null) + SizedBox( + height: 24, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + padding: + const EdgeInsets.symmetric( + horizontal: 6, + ), + ), + icon: const Icon( + Icons.calendar_month, + size: 16, + ), + label: Text( + DateFormat.yMMMd(tag) + .format(dueDate), + style: const TextStyle( + fontSize: 12, + ), + ), + onPressed: () => + controller.editTodoDueDate( + i, + todo, + ), + ), + ), + ], ), + onTap: controller.isLoading + ? null + : () => controller.toggleDone(i), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon( + Icons.edit_outlined, + size: 16, + ), + onPressed: () => + controller.editTodo(i, todo), + ), + IconButton( + icon: const Icon( + Icons.delete_outlined, + size: 16, + ), + onPressed: () => controller.deleteTodo(i), + ), + const SizedBox(width: 8), ], ), - onTap: controller.isLoading - ? null - : () => controller.toggleDone(i), - trailing: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: IconButton( - icon: const Icon(Icons.edit_outlined, size: 16), - onPressed: () => controller.editTodo(i, todo), - ), + ); + }, ), - ); - }, - ); - }, - ), - ), - ), - Padding( - padding: const EdgeInsets.all(12.0), - child: TextField( - focusNode: controller.focusNode, - readOnly: controller.isLoading, - controller: controller.textEditingController, - onSubmitted: controller.addTodo, - maxLength: 64, - decoration: InputDecoration( - counterStyle: const TextStyle(height: double.minPositive), - counterText: '', - hintText: L10n.of(context)!.newTodo, - prefixIcon: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - icon: Icon( - controller.newTaskDateTime == null - ? Icons.calendar_month_outlined - : Icons.calendar_month, - color: controller.newTaskDateTime == null - ? null - : Theme.of(context).primaryColor, - ), - onPressed: controller.setNewTaskDateTime, - ), - IconButton( - icon: Icon( - Icons.text_fields, - color: controller.newTaskDescription == null - ? null - : Theme.of(context).primaryColor, - ), - onPressed: controller.setNewTaskDescription, - ), - ], - ), - suffixIcon: IconButton( - icon: const Icon(Icons.add_outlined), - onPressed: controller.isLoading ? null : controller.addTodo, ), ), - ), + Padding( + padding: const EdgeInsets.all(12.0), + child: TextField( + focusNode: controller.focusNode, + readOnly: controller.isLoading, + controller: controller.textEditingController, + onSubmitted: controller.addTodo, + maxLength: 64, + decoration: InputDecoration( + counterStyle: const TextStyle(height: double.minPositive), + counterText: '', + hintText: L10n.of(context)!.newTodo, + prefixIcon: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + controller.newTaskDateTime == null + ? Icons.calendar_month_outlined + : Icons.calendar_month, + color: controller.newTaskDateTime == null + ? null + : Theme.of(context).primaryColor, + ), + onPressed: controller.setNewTaskDateTime, + ), + IconButton( + icon: Icon( + Icons.text_fields, + color: controller.newTaskDescription == null + ? null + : Theme.of(context).primaryColor, + ), + onPressed: controller.setNewTaskDescription, + ), + ], + ), + suffixIcon: IconButton( + icon: const Icon(Icons.add_outlined), + onPressed: + controller.isLoading ? null : controller.addTodo, + ), + ), + ), + ), + ], ), - ], - ), + ); + }, ); } }