From 62d54905bf9b29595f269718284855a2beda7800 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sat, 9 Aug 2025 21:49:19 +0200 Subject: [PATCH 01/11] :sparkles: Add optional 3-dot-menu to appBar --- lib/ui/_widgets/app_bar.dart | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/ui/_widgets/app_bar.dart b/lib/ui/_widgets/app_bar.dart index d93134b6..e86b27fe 100644 --- a/lib/ui/_widgets/app_bar.dart +++ b/lib/ui/_widgets/app_bar.dart @@ -7,10 +7,17 @@ import "package:gitdone/ui/_widgets/app_title.dart"; /// A normal app bar that is used in most of the views. class NormalAppBar extends StatelessWidget implements PreferredSizeWidget { /// Creates a normal app bar with an optional back button. - const NormalAppBar({super.key, this.backVisible = true}); + const NormalAppBar({ + super.key, + this.backVisible = true, + this.menuItems = const [], + }); /// Whether the back button should be visible. - final dynamic backVisible; + final bool backVisible; + + /// The actions to be displayed in the app bar om PopupMenuButton. + final List menuItems; @override Widget build(final BuildContext context) => AppBar( @@ -26,7 +33,25 @@ class NormalAppBar extends StatelessWidget implements PreferredSizeWidget { onPressed: Navigation.navigateBack, ) : const SizedBox(width: 48), - actions: const [SizedBox(width: 48)], + actions: [ + if (menuItems.isNotEmpty) _buildMenu() else const SizedBox(width: 48), + ], + ); + + Widget _buildMenu() => MenuAnchor( + builder: (final context, final controller, final child) => IconButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + icon: const Icon(Icons.more_vert), + tooltip: "Show menu", + ), + alignmentOffset: const Offset(0, -4), // First item exactly below app bar + menuChildren: menuItems, ); @override From febdd279fd8d5853d180fea8ec36ddf6b3bac106 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sat, 9 Aug 2025 21:54:49 +0200 Subject: [PATCH 02/11] :sparkles: Add delete task button to details view --- lib/ui/task_details/task_details_view.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/ui/task_details/task_details_view.dart b/lib/ui/task_details/task_details_view.dart index cc94ad93..99b9f5e2 100644 --- a/lib/ui/task_details/task_details_view.dart +++ b/lib/ui/task_details/task_details_view.dart @@ -34,7 +34,7 @@ class _TaskDetailsViewState extends State { @override Widget build(final BuildContext context) => Scaffold( - appBar: const NormalAppBar(), + appBar: NormalAppBar(menuItems: [_deleteTaskButton()]), body: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( @@ -113,6 +113,11 @@ class _TaskDetailsViewState extends State { ); } + MenuItemButton _deleteTaskButton() => MenuItemButton( + onPressed: () => debugPrint("Delete task: ${widget.task.title}"), + child: const Text("Delete Task"), + ); + String _formatDateTime(final DateTime dateTime) => DateFormat("yyyy-MM-dd HH:mm:ss").format(dateTime.toLocal()); } From 81c3f6f70a9f2e8b04a6f626b066fe375920be4a Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sat, 9 Aug 2025 21:56:19 +0200 Subject: [PATCH 03/11] :recycle: Move task edit and delete functionality to task details view model --- lib/ui/task_details/task_details_view.dart | 79 ++++++++----------- .../task_details/task_details_view_model.dart | 34 ++++++++ 2 files changed, 66 insertions(+), 47 deletions(-) create mode 100644 lib/ui/task_details/task_details_view_model.dart diff --git a/lib/ui/task_details/task_details_view.dart b/lib/ui/task_details/task_details_view.dart index 99b9f5e2..982132d9 100644 --- a/lib/ui/task_details/task_details_view.dart +++ b/lib/ui/task_details/task_details_view.dart @@ -1,14 +1,13 @@ import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:gitdone/core/models/task.dart"; -import "package:gitdone/core/utils/logger.dart"; -import "package:gitdone/core/utils/navigation.dart"; import "package:gitdone/ui/_widgets/app_bar.dart"; import "package:gitdone/ui/_widgets/page_title.dart"; import "package:gitdone/ui/_widgets/task_labels.dart"; -import "package:gitdone/ui/task_edit/task_edit_view.dart"; +import "package:gitdone/ui/task_details/task_details_view_model.dart"; import "package:intl/intl.dart"; import "package:markdown_widget/markdown_widget.dart"; +import "package:provider/provider.dart"; /// A widget that displays a card for a task item. class TaskDetailsView extends StatefulWidget { @@ -29,32 +28,34 @@ class TaskDetailsView extends StatefulWidget { } class _TaskDetailsViewState extends State { - static const _classId = - "com.GitDone.gitdone.ui.task_details.task_details_view_model"; - @override - Widget build(final BuildContext context) => Scaffold( - appBar: NormalAppBar(menuItems: [_deleteTaskButton()]), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _renderTitle(), - _renderLabels(), - const Padding(padding: EdgeInsets.all(8)), - _renderDescription(), - const Padding(padding: EdgeInsets.all(8)), - _renderStatus(), - const Padding(padding: EdgeInsets.all(8)), - ], + Widget build(final BuildContext context) => ChangeNotifierProvider( + create: (_) => TaskDetailsViewModel(widget.task), + child: Consumer( + builder: (final context, final viewModel, _) => Scaffold( + appBar: NormalAppBar(menuItems: [_deleteTaskButton(viewModel)]), + body: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _renderTitle(), + _renderLabels(), + const Padding(padding: EdgeInsets.all(8)), + _renderDescription(), + const Padding(padding: EdgeInsets.all(8)), + _renderStatus(), + const Padding(padding: EdgeInsets.all(8)), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: viewModel.editTask, + child: const Icon(Icons.edit), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ), ), - floatingActionButton: FloatingActionButton( - onPressed: _editTask, - child: const Icon(Icons.edit), - ), - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, ); Widget _renderTitle() => PageTitleWidget(title: widget.task.title); @@ -96,27 +97,11 @@ class _TaskDetailsViewState extends State { ), ); - Future _editTask() async { - Logger.log("Edit task: ${widget.task.title}", _classId, LogLevel.detailed); - final Task? updated = await Navigation.navigate(TaskEditView(widget.task)); - if (updated == null) { - Logger.log("Task edit cancelled or failed", _classId, LogLevel.detailed); - return; - } - setState(() { - widget.task.replace(updated); - }); - Logger.log( - "Task updated: ${widget.task.title}", - _classId, - LogLevel.detailed, - ); - } - - MenuItemButton _deleteTaskButton() => MenuItemButton( - onPressed: () => debugPrint("Delete task: ${widget.task.title}"), - child: const Text("Delete Task"), - ); + MenuItemButton _deleteTaskButton(final TaskDetailsViewModel viewModel) => + MenuItemButton( + onPressed: viewModel.deleteTask, + child: const Text("Delete Task"), + ); String _formatDateTime(final DateTime dateTime) => DateFormat("yyyy-MM-dd HH:mm:ss").format(dateTime.toLocal()); diff --git a/lib/ui/task_details/task_details_view_model.dart b/lib/ui/task_details/task_details_view_model.dart new file mode 100644 index 00000000..0da11fc5 --- /dev/null +++ b/lib/ui/task_details/task_details_view_model.dart @@ -0,0 +1,34 @@ +import "package:flutter/foundation.dart"; +import "package:gitdone/core/models/task.dart"; +import "package:gitdone/core/utils/logger.dart"; +import "package:gitdone/core/utils/navigation.dart"; +import "package:gitdone/ui/task_edit/task_edit_view.dart"; + +/// A view model for the task details view. +class TaskDetailsViewModel extends ChangeNotifier { + /// Creates a [TaskDetailsViewModel] with the given task item. + TaskDetailsViewModel(this._task); + + final Task _task; + + static const _classId = + "com.GitDone.gitdone.ui.task_details.task_details_view_model"; + + /// Starts editing the task. + Future editTask() async { + Logger.log("Edit task: ${_task.title}", _classId, LogLevel.detailed); + final Task? updated = await Navigation.navigate(TaskEditView(_task)); + if (updated == null) { + Logger.log("Task edit cancelled or failed", _classId, LogLevel.detailed); + return; + } + _task.replace(updated); + notifyListeners(); + Logger.log("Task updated: ${_task.title}", _classId, LogLevel.detailed); + } + + /// Deletes the task. + Future deleteTask() async { + Logger.log("Delete task: ${_task.title}", _classId, LogLevel.detailed); + } +} From 19323ef07cdf29d6903e73e9f38f4176a8c66262 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:50:58 +0200 Subject: [PATCH 04/11] :heavy_plus_sign: Add transitive dependencies from github_flutter --- pubspec.lock | 136 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) diff --git a/pubspec.lock b/pubspec.lock index e73ba0e8..b888620a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -207,6 +215,70 @@ packages: relative: true source: path version: "0.0.1" + gql: + dependency: transitive + description: + name: gql + sha256: "650e79ed60c21579ca3bd17ebae8a8c8d22cde267b03a19bf3b35996baaa843a" + url: "https://pub.dev" + source: hosted + version: "1.0.1-alpha+1730759315362" + gql_dedupe_link: + dependency: transitive + description: + name: gql_dedupe_link + sha256: "10bee0564d67c24e0c8bd08bd56e0682b64a135e58afabbeed30d85d5e9fea96" + url: "https://pub.dev" + source: hosted + version: "2.0.4-alpha+1715521079596" + gql_error_link: + dependency: transitive + description: + name: gql_error_link + sha256: "93901458f3c050e33386dedb0ca7173e08cebd7078e4e0deca4bf23ab7a71f63" + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" + gql_exec: + dependency: transitive + description: + name: gql_exec + sha256: "394944626fae900f1d34343ecf2d62e44eb984826189c8979d305f0ae5846e38" + url: "https://pub.dev" + source: hosted + version: "1.1.1-alpha+1699813812660" + gql_http_link: + dependency: transitive + description: + name: gql_http_link + sha256: ef6ad24d31beb5a30113e9b919eec20876903cc4b0ee0d31550047aaaba7d5dd + url: "https://pub.dev" + source: hosted + version: "1.1.0" + gql_link: + dependency: transitive + description: + name: gql_link + sha256: c2b0adb2f6a60c2599b9128fb095316db5feb99ce444c86fb141a6964acedfa4 + url: "https://pub.dev" + source: hosted + version: "1.0.1-alpha+1730759315378" + gql_transform_link: + dependency: transitive + description: + name: gql_transform_link + sha256: "0645fdd874ca1be695fd327271fdfb24c0cd6fa40774a64b946062f321a59709" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + graphql: + dependency: transitive + description: + name: graphql + sha256: "735bbbaa4db10d38054932e726d291bdd46e46e0575cd482a74b0615b8622e1c" + url: "https://pub.dev" + source: hosted + version: "5.2.1" highlight: dependency: transitive description: @@ -215,6 +287,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" http: dependency: "direct main" description: @@ -351,6 +431,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + normalize: + dependency: transitive + description: + name: normalize + sha256: f78bf0552b9640c76369253f0b8fdabad4f3fbfc06bdae9359e71bee9a5b071b + url: "https://pub.dev" + source: hosted + version: "0.9.1" package_info_plus: dependency: "direct main" description: @@ -463,6 +551,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.1.5" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" scroll_to_index: dependency: transitive description: @@ -540,6 +636,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -652,6 +756,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -708,6 +820,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" win32: dependency: transitive description: @@ -740,6 +868,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.5.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" sdks: dart: ">=3.8.0 <4.0.0" flutter: ">=3.27.0" From cbd656a87cc985743af9ca133b27f937bbc9d784 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:58:29 +0200 Subject: [PATCH 05/11] :sparkles: Implement functionality to delete tasks --- lib/core/models/task.dart | 14 ++++++++++++++ lib/core/task_handler.dart | 16 ++++++++++++++-- lib/ui/task_details/task_details_view_model.dart | 3 +++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/core/models/task.dart b/lib/core/models/task.dart index 263fc45e..46652bc9 100644 --- a/lib/core/models/task.dart +++ b/lib/core/models/task.dart @@ -106,6 +106,20 @@ class Task { return Task.fromIssue(issue, slug); }); + /// Deletes the issue from the remote repository. + Future deleteRemote() async { + if (_issueNumber == null) { + Logger.logWarning( + "Cannot delete issue $_issueNumber, it does not exist", + _classId, + ); + return; + } + Logger.logInfo("Deleting issue $_issueNumber", _classId); + await (await GithubModel.github).issues.deleteIssue(_slug, _issueNumber); + Logger.logInfo("Successfully deleted issue $_issueNumber", _classId); + } + /// Creates a copy of the current instance. Task copy() => Task( title: title, diff --git a/lib/core/task_handler.dart b/lib/core/task_handler.dart index 30752589..4c3bf128 100644 --- a/lib/core/task_handler.dart +++ b/lib/core/task_handler.dart @@ -98,8 +98,7 @@ class TaskHandler extends ChangeNotifier { return task; } - /// Updates an existing task in the list of tasks. Notifies listeners - /// about the change. + /// Updates an existing task in the list of tasks. Notifies listeners about the change. /// Does not save the task to the remote repository! void updateLocalTask(final Task task) { final int index = _tasks.indexWhere( @@ -116,6 +115,19 @@ class TaskHandler extends ChangeNotifier { } } + /// Deletes a task from the list of tasks. Notifies listeners about the change. + /// Also deletes the task from the remote repository. + /// If the task does not exist in the remote repository, it will be removed from the local list. + Future deleteTask(final Task task) async { + try { + await task.deleteRemote(); + _tasks.removeWhere((final t) => t.issueNumber == task.issueNumber); + notifyListeners(); + } on Exception catch (e) { + Logger.logError("Failed to delete task", _classId, e); + } + } + Future> _fetchIssuesForRepository( final RepositoryDetails repo, ) async => (await GithubModel.github).issues diff --git a/lib/ui/task_details/task_details_view_model.dart b/lib/ui/task_details/task_details_view_model.dart index 0da11fc5..b8ce16d9 100644 --- a/lib/ui/task_details/task_details_view_model.dart +++ b/lib/ui/task_details/task_details_view_model.dart @@ -1,5 +1,6 @@ import "package:flutter/foundation.dart"; import "package:gitdone/core/models/task.dart"; +import "package:gitdone/core/task_handler.dart"; import "package:gitdone/core/utils/logger.dart"; import "package:gitdone/core/utils/navigation.dart"; import "package:gitdone/ui/task_edit/task_edit_view.dart"; @@ -30,5 +31,7 @@ class TaskDetailsViewModel extends ChangeNotifier { /// Deletes the task. Future deleteTask() async { Logger.log("Delete task: ${_task.title}", _classId, LogLevel.detailed); + // TODO: Implement deletion confirmation dialog + TaskHandler().deleteTask(_task); } } From e15e6844dfbd08bb60718431cc24cb64ffe7ba58 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sun, 17 Aug 2025 16:14:49 +0200 Subject: [PATCH 06/11] :art: Fix todo style --- lib/ui/task_details/task_details_view_model.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/task_details/task_details_view_model.dart b/lib/ui/task_details/task_details_view_model.dart index d4146967..aea03731 100644 --- a/lib/ui/task_details/task_details_view_model.dart +++ b/lib/ui/task_details/task_details_view_model.dart @@ -32,7 +32,7 @@ class TaskDetailsViewModel extends ChangeNotifier { /// Deletes the task. Future deleteTask() async { Logger.log("Delete task: ${task.title}", _classId, LogLevel.detailed); - // TODO: Implement deletion confirmation dialog + // TODO(all): Implement deletion confirmation dialog TaskHandler().deleteTask(task); } From 9ae2a20289e237d3b6ab4113b638b52d78653bba Mon Sep 17 00:00:00 2001 From: Florian Barth Date: Sat, 6 Sep 2025 19:57:44 +0200 Subject: [PATCH 07/11] :sparkles: navigate back after task deletion --- lib/ui/task_details/task_details_view_model.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/ui/task_details/task_details_view_model.dart b/lib/ui/task_details/task_details_view_model.dart index aea03731..9aa5c781 100644 --- a/lib/ui/task_details/task_details_view_model.dart +++ b/lib/ui/task_details/task_details_view_model.dart @@ -33,7 +33,8 @@ class TaskDetailsViewModel extends ChangeNotifier { Future deleteTask() async { Logger.log("Delete task: ${task.title}", _classId, LogLevel.detailed); // TODO(all): Implement deletion confirmation dialog - TaskHandler().deleteTask(task); + await TaskHandler().deleteTask(task); + Navigation.navigateBack(); } /// Marks the task as done. From 91ae3f467830c7e2a63891a47098ee206e8f6d23 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sat, 6 Sep 2025 22:13:26 +0200 Subject: [PATCH 08/11] :arrow_up: Update transitive dependencies --- pubspec.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index b888620a..a6e62d77 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -339,26 +339,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -680,10 +680,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" typed_data: dependency: transitive description: @@ -792,10 +792,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" visibility_detector: dependency: transitive description: From c0a601af1504e0ae4eb8aed1434c2fef221215ff Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sat, 6 Sep 2025 22:59:19 +0200 Subject: [PATCH 09/11] :sparkles: Add confirm delete dialog --- lib/ui/_widgets/confirm_dialog.dart | 73 +++++++++++++++++++ lib/ui/task_details/task_details_view.dart | 20 ++++- .../task_details/task_details_view_model.dart | 3 +- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 lib/ui/_widgets/confirm_dialog.dart diff --git a/lib/ui/_widgets/confirm_dialog.dart b/lib/ui/_widgets/confirm_dialog.dart new file mode 100644 index 00000000..5e01f985 --- /dev/null +++ b/lib/ui/_widgets/confirm_dialog.dart @@ -0,0 +1,73 @@ +import "package:flutter/material.dart"; +import "package:gitdone/core/utils/navigation.dart"; + +/// A dialog that asks the user to confirm an action. +class ConfirmDialog extends StatelessWidget { + /// Creates a [ConfirmDialog] widget with the given parameters. + const ConfirmDialog({ + required final Widget title, + required final Widget content, + required final String confirmText, + required final VoidCallback onConfirm, + required final String cancelText, + final VoidCallback? onCancel, + super.key, + }) : _title = title, + _content = content, + _confirmText = confirmText, + _cancelText = cancelText, + _onConfirm = onConfirm, + _onCancel = onCancel; + + final Widget _title; + final Widget _content; + final String _confirmText; + final String _cancelText; + final VoidCallback _onConfirm; + final VoidCallback? _onCancel; + + @override + Widget build(final BuildContext context) => AlertDialog( + title: _title, + content: _content, + actions: [ + TextButton( + onPressed: () { + Navigation.navigateBack(); + _onConfirm(); + }, + child: Text(_confirmText), + ), + TextButton( + onPressed: () { + Navigation.navigateBack(); + if (_onCancel != null) _onCancel(); + }, + child: Text(_cancelText), + ), + ], + ); + + /// Shows the ConfirmDialog as a dialog. + static Future show( + final BuildContext context, { + required final Widget title, + required final Widget content, + required final String confirmText, + required final VoidCallback onConfirm, + final String cancelText = "Cancel", + final VoidCallback? onCancel, + final bool barrierDismissible = true, + }) => showDialog( + context: context, + barrierDismissible: barrierDismissible, + builder: (final context) => ConfirmDialog( + title: title, + content: content, + confirmText: confirmText, + cancelText: cancelText, + onConfirm: onConfirm, + onCancel: onCancel, + ), + ); +} diff --git a/lib/ui/task_details/task_details_view.dart b/lib/ui/task_details/task_details_view.dart index fc5f2cbe..2df97dfd 100644 --- a/lib/ui/task_details/task_details_view.dart +++ b/lib/ui/task_details/task_details_view.dart @@ -2,7 +2,9 @@ import "package:flutter/foundation.dart"; import "package:flutter/material.dart"; import "package:gitdone/core/models/task.dart"; import "package:gitdone/core/task_handler.dart"; +import "package:gitdone/core/utils/navigation.dart"; import "package:gitdone/ui/_widgets/app_bar.dart"; +import "package:gitdone/ui/_widgets/confirm_dialog.dart"; import "package:gitdone/ui/_widgets/page_title.dart"; import "package:gitdone/ui/_widgets/task_labels.dart"; import "package:gitdone/ui/task_details/task_details_view_model.dart"; @@ -118,7 +120,23 @@ class _TaskDetailsViewState extends State { MenuItemButton _deleteTaskButton(final TaskDetailsViewModel viewModel) => MenuItemButton( - onPressed: viewModel.deleteTask, + onPressed: () => ConfirmDialog.show( + context, + title: const Text("Delete Task"), + content: const SingleChildScrollView( + child: ListBody( + children: [ + Text("Are you sure you want to delete this task?"), + Text("This action cannot be undone."), + ], + ), + ), + confirmText: "Delete", + onConfirm: () async { + await viewModel.deleteTask(); + Navigation.navigateBack(); + }, + ), child: const Text("Delete Task"), ); diff --git a/lib/ui/task_details/task_details_view_model.dart b/lib/ui/task_details/task_details_view_model.dart index 9aa5c781..ec2b622e 100644 --- a/lib/ui/task_details/task_details_view_model.dart +++ b/lib/ui/task_details/task_details_view_model.dart @@ -4,6 +4,7 @@ import "package:gitdone/core/task_handler.dart"; import "package:gitdone/core/utils/logger.dart"; import "package:gitdone/core/utils/navigation.dart"; import "package:gitdone/ui/task_edit/task_edit_view.dart"; +import "package:gitdone/ui/task_list/task_list_view.dart"; /// A view model for the task details view. class TaskDetailsViewModel extends ChangeNotifier { @@ -34,7 +35,7 @@ class TaskDetailsViewModel extends ChangeNotifier { Logger.log("Delete task: ${task.title}", _classId, LogLevel.detailed); // TODO(all): Implement deletion confirmation dialog await TaskHandler().deleteTask(task); - Navigation.navigateBack(); + Navigation.navigateReplace(const TaskListView()); } /// Marks the task as done. From 172700ea481dc5f5dda70ce3e8d0c65fd0572d30 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:13:56 +0200 Subject: [PATCH 10/11] :sparkles: Show delete success or fail snack bar --- lib/core/task_handler.dart | 4 +++- lib/core/utils/snack_bar.dart | 14 ++++++++++++++ lib/ui/task_details/task_details_view.dart | 17 +++++++++++++++-- .../task_details/task_details_view_model.dart | 7 ++----- 4 files changed, 34 insertions(+), 8 deletions(-) create mode 100644 lib/core/utils/snack_bar.dart diff --git a/lib/core/task_handler.dart b/lib/core/task_handler.dart index 6802a2c2..4fde4c31 100644 --- a/lib/core/task_handler.dart +++ b/lib/core/task_handler.dart @@ -118,14 +118,16 @@ class TaskHandler extends ChangeNotifier { /// Deletes a task from the list of tasks. Notifies listeners about the change. /// Also deletes the task from the remote repository. /// If the task does not exist in the remote repository, it will be removed from the local list. - Future deleteTask(final Task task) async { + Future deleteTask(final Task task) async { try { await task.deleteRemote(); _tasks.removeWhere((final t) => t.issueNumber == task.issueNumber); notifyListeners(); + return true; } on Exception catch (e) { Logger.logError("Failed to delete task", _classId, e); } + return false; } Future> _fetchIssuesForRepository( diff --git a/lib/core/utils/snack_bar.dart b/lib/core/utils/snack_bar.dart new file mode 100644 index 00000000..3018cf12 --- /dev/null +++ b/lib/core/utils/snack_bar.dart @@ -0,0 +1,14 @@ +import "package:flutter/material.dart"; + +/// Utility class for showing Snack bars safely. +class SnackBarUtils { + /// Shows a SnackBar with the given [message]. + static void show({ + required final BuildContext context, + required final String message, + }) { + ScaffoldMessenger.of(context) + ..hideCurrentSnackBar() + ..showSnackBar(SnackBar(content: Text(message))); + } +} diff --git a/lib/ui/task_details/task_details_view.dart b/lib/ui/task_details/task_details_view.dart index 2df97dfd..f739bfcb 100644 --- a/lib/ui/task_details/task_details_view.dart +++ b/lib/ui/task_details/task_details_view.dart @@ -3,6 +3,7 @@ import "package:flutter/material.dart"; import "package:gitdone/core/models/task.dart"; import "package:gitdone/core/task_handler.dart"; import "package:gitdone/core/utils/navigation.dart"; +import "package:gitdone/core/utils/snack_bar.dart"; import "package:gitdone/ui/_widgets/app_bar.dart"; import "package:gitdone/ui/_widgets/confirm_dialog.dart"; import "package:gitdone/ui/_widgets/page_title.dart"; @@ -133,8 +134,20 @@ class _TaskDetailsViewState extends State { ), confirmText: "Delete", onConfirm: () async { - await viewModel.deleteTask(); - Navigation.navigateBack(); + final bool success = await viewModel.deleteTask(); + if (!mounted) return; + if (success) { + SnackBarUtils.show( + context: context, + message: "Task deleted successfully.", + ); + Navigation.navigateBack(); + } else { + SnackBarUtils.show( + context: context, + message: "Failed to delete task.", + ); + } }, ), child: const Text("Delete Task"), diff --git a/lib/ui/task_details/task_details_view_model.dart b/lib/ui/task_details/task_details_view_model.dart index ec2b622e..2565003e 100644 --- a/lib/ui/task_details/task_details_view_model.dart +++ b/lib/ui/task_details/task_details_view_model.dart @@ -4,7 +4,6 @@ import "package:gitdone/core/task_handler.dart"; import "package:gitdone/core/utils/logger.dart"; import "package:gitdone/core/utils/navigation.dart"; import "package:gitdone/ui/task_edit/task_edit_view.dart"; -import "package:gitdone/ui/task_list/task_list_view.dart"; /// A view model for the task details view. class TaskDetailsViewModel extends ChangeNotifier { @@ -31,11 +30,9 @@ class TaskDetailsViewModel extends ChangeNotifier { } /// Deletes the task. - Future deleteTask() async { + Future deleteTask() { Logger.log("Delete task: ${task.title}", _classId, LogLevel.detailed); - // TODO(all): Implement deletion confirmation dialog - await TaskHandler().deleteTask(task); - Navigation.navigateReplace(const TaskListView()); + return TaskHandler().deleteTask(task); } /// Marks the task as done. From 598f11258c75e9acedfc9ac5c85f89903dddda22 Mon Sep 17 00:00:00 2001 From: Friedinger <71267611+Friedinger@users.noreply.github.com> Date: Sun, 7 Sep 2025 13:16:50 +0200 Subject: [PATCH 11/11] :recycle: Use snack bar utils in repository selector --- .../repository_selector/repository_selector_view.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ui/settings/widgets/repository_selector/repository_selector_view.dart b/lib/ui/settings/widgets/repository_selector/repository_selector_view.dart index d160e2d2..ad9bd8b9 100644 --- a/lib/ui/settings/widgets/repository_selector/repository_selector_view.dart +++ b/lib/ui/settings/widgets/repository_selector/repository_selector_view.dart @@ -1,5 +1,6 @@ import "package:flutter/material.dart"; import "package:gitdone/core/models/repository_details.dart"; +import "package:gitdone/core/utils/snack_bar.dart"; import "package:gitdone/ui/settings/widgets/repository_selector/repository_selector_view_model.dart"; import "package:provider/provider.dart"; @@ -46,9 +47,9 @@ class _RepositorySelectorState extends State { model ..selectRepository(repo) ..saveSelectedRepository(); - ScaffoldMessenger.of(context).hideCurrentSnackBar(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("Selected repository: ${repo?.name}")), + SnackBarUtils.show( + context: context, + message: "Selected repository: ${repo?.name}", ); }, ),