Skip to content

Commit df3a514

Browse files
authored
🚸 Add confirmation dialog before changing task state (#416)
2 parents 5e86093 + 6002453 commit df3a514

12 files changed

Lines changed: 335 additions & 121 deletions

‎lib/core/settings_handler.dart‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ import "package:shared_preferences/shared_preferences.dart";
99
enum SettingKey {
1010
/// Key for storing the selected repository details.
1111
selectedRepository,
12+
13+
/// Key for storing whether to show the confirmation dialog before marking a task as done or open.
14+
showMarkTaskStateConfirmation,
1215
}
1316

1417
/// Centralized handler for application settings.
1518
class SettingsHandler extends ChangeNotifier {
1619
/// Creates a new instance of [SettingsHandler].
1720
factory SettingsHandler() => _instance;
21+
1822
SettingsHandler._internal();
23+
1924
static final SettingsHandler _instance = SettingsHandler._internal();
2025
static const _classId = "com.GitDone.gitdone.core.settings_handler";
2126

@@ -89,6 +94,20 @@ class SettingsHandler extends ChangeNotifier {
8994
}
9095
}
9196

97+
/// Returns whether to show the confirmation dialog before marking a task as done or open.
98+
bool showMarkTaskStateConfirmation() {
99+
_ensurePrefsInitialized();
100+
return _prefs?.getBool(SettingKey.showMarkTaskStateConfirmation.name) ??
101+
true;
102+
}
103+
104+
/// Sets whether to show the confirmation dialog before marking a task as done or open.
105+
Future<void> setShowMarkTaskStateConfirmation({required bool value}) async {
106+
_ensurePrefsInitialized();
107+
await _prefs?.setBool(SettingKey.showMarkTaskStateConfirmation.name, value);
108+
notifyListeners();
109+
}
110+
92111
/// Removes the [key] setting from persistent storage.
93112
Future<void> remove(final SettingKey key) async {
94113
_ensurePrefsInitialized();

‎lib/ui/_widgets/confirm_dialog.dart‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,26 @@ class ConfirmDialog extends StatelessWidget {
3333
actions: <Widget>[
3434
TextButton(
3535
onPressed: () {
36-
Navigation.navigateBack();
37-
_onConfirm();
36+
Navigation.navigateBack(false);
37+
if (_onCancel != null) _onCancel();
3838
},
39-
child: Text(_confirmText),
39+
child: Text(_cancelText),
4040
),
41-
TextButton(
41+
FilledButton(
4242
onPressed: () {
43-
Navigation.navigateBack();
44-
if (_onCancel != null) _onCancel();
43+
Navigation.navigateBack(true);
44+
_onConfirm();
4545
},
46-
child: Text(_cancelText),
46+
style: FilledButton.styleFrom(
47+
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 12),
48+
),
49+
child: Text(_confirmText),
4750
),
4851
],
4952
);
5053

5154
/// Shows the ConfirmDialog as a dialog.
52-
static Future<void> show(
55+
static Future<bool?> show(
5356
final BuildContext context, {
5457
required final Widget title,
5558
required final Widget content,
@@ -58,7 +61,7 @@ class ConfirmDialog extends StatelessWidget {
5861
final String cancelText = "Cancel",
5962
final VoidCallback? onCancel,
6063
final bool barrierDismissible = true,
61-
}) => showDialog<void>(
64+
}) => showDialog<bool>(
6265
context: context,
6366
barrierDismissible: barrierDismissible,
6467
builder: (final context) => ConfirmDialog(
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import "package:flutter/material.dart";
2+
import "package:gitdone/core/settings_handler.dart";
3+
import "package:gitdone/core/task_handler.dart";
4+
import "package:gitdone/ui/_widgets/confirm_dialog.dart";
5+
6+
/// Shows a confirmation dialog for marking a task as done or open.
7+
///
8+
/// If the "don't ask again" setting is enabled, it will execute [onConfirm] immediately.
9+
Future<void> showMarkTaskConfirmationDialog({
10+
required final BuildContext context,
11+
required final String currentTaskState,
12+
required final VoidCallback onConfirm,
13+
}) async {
14+
final _DialogTexts texts = currentTaskState == IssueState.open.value
15+
? (
16+
title: "Mark as done",
17+
description:
18+
"Are you sure you want to mark this task as done? This will close the GitHub issue.",
19+
confirmText: "Mark as done",
20+
)
21+
: (
22+
title: "Reopen task",
23+
description: "Are you sure you want to reopen this task?",
24+
confirmText: "Reopen",
25+
);
26+
27+
if (!SettingsHandler().showMarkTaskStateConfirmation()) {
28+
onConfirm();
29+
return;
30+
}
31+
32+
bool doNotAskAgain = false;
33+
await showDialog(
34+
context: context,
35+
builder: (final context) => StatefulBuilder(
36+
builder: (final context, final setState) => ConfirmDialog(
37+
title: Text(texts.title),
38+
content: _buildDialogContent(
39+
description: texts.description,
40+
doNotAskAgain: doNotAskAgain,
41+
onChanged: (final value) => setState(() => doNotAskAgain = value),
42+
),
43+
confirmText: texts.confirmText,
44+
onConfirm: () async {
45+
if (doNotAskAgain) {
46+
await SettingsHandler().setShowMarkTaskStateConfirmation(
47+
value: false,
48+
);
49+
}
50+
onConfirm();
51+
},
52+
cancelText: "Cancel",
53+
),
54+
),
55+
);
56+
}
57+
58+
Widget _buildDialogContent({
59+
required final String description,
60+
required final bool doNotAskAgain,
61+
required final ValueChanged<bool> onChanged,
62+
}) => Column(
63+
mainAxisSize: MainAxisSize.min,
64+
crossAxisAlignment: CrossAxisAlignment.start,
65+
children: [
66+
Text(description),
67+
Row(
68+
children: [
69+
Checkbox(
70+
value: doNotAskAgain,
71+
onChanged: (final value) => onChanged(value ?? false),
72+
),
73+
const Text("Don't ask me again"),
74+
],
75+
),
76+
],
77+
);
78+
79+
typedef _DialogTexts = ({String title, String description, String confirmText});

‎lib/ui/_widgets/task_card.dart‎

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -26,31 +26,29 @@ class TaskCard extends StatefulWidget {
2626

2727
class _TaskCardState extends State<TaskCard> {
2828
@override
29-
Widget build(final BuildContext context) => Padding(
30-
padding: const EdgeInsets.all(4),
31-
child: GestureDetector(
32-
onTap: () => Navigation.navigate(TaskDetailsView(widget.task)),
33-
child: Card(
34-
color: AppColor.colorScheme.surfaceContainer,
35-
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
36-
child: Padding(
37-
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
38-
child: Row(
39-
spacing: 16,
40-
children: [
41-
const Placeholder(fallbackHeight: 50, fallbackWidth: 50),
42-
Expanded(
43-
child: Column(
44-
crossAxisAlignment: CrossAxisAlignment.start,
45-
spacing: 4,
46-
children: [
47-
_buildTitle(widget.task.title),
48-
TaskLabels(widget.task),
49-
],
50-
),
29+
Widget build(final BuildContext context) => GestureDetector(
30+
onTap: () => Navigation.navigate(TaskDetailsView(widget.task)),
31+
child: Card(
32+
color: AppColor.colorScheme.surfaceContainer,
33+
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
34+
margin: EdgeInsets.zero,
35+
child: Padding(
36+
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
37+
child: Row(
38+
spacing: 16,
39+
children: [
40+
const Placeholder(fallbackHeight: 50, fallbackWidth: 50),
41+
Expanded(
42+
child: Column(
43+
crossAxisAlignment: CrossAxisAlignment.start,
44+
spacing: 4,
45+
children: [
46+
_buildTitle(widget.task.title),
47+
TaskLabels(widget.task),
48+
],
5149
),
52-
],
53-
),
50+
),
51+
],
5452
),
5553
),
5654
),

‎lib/ui/settings/settings_view.dart‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import "package:flutter/material.dart";
22
import "package:gitdone/app_environment.dart";
33
import "package:gitdone/ui/_widgets/page_title.dart";
44
import "package:gitdone/ui/settings/widgets/account_management.dart";
5+
import "package:gitdone/ui/settings/widgets/general_settings.dart";
56
import "package:gitdone/ui/settings/widgets/import_export/import_export_settings.dart";
67
import "package:gitdone/ui/settings/widgets/repository_selector/repository_selector_view.dart";
78

@@ -21,6 +22,7 @@ class SettingsView extends StatelessWidget {
2122
crossAxisAlignment: CrossAxisAlignment.start,
2223
spacing: 16,
2324
children: [
25+
const GeneralSettings(),
2426
Column(
2527
crossAxisAlignment: CrossAxisAlignment.start,
2628
children: [
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import "package:flutter/material.dart";
2+
import "package:gitdone/core/settings_handler.dart";
3+
4+
/// A setting widget that allows users to toggle the confirmation dialog for task status changes.
5+
class ConfirmTaskStatusChangeSetting extends StatelessWidget {
6+
/// Creates an instance of [ConfirmTaskStatusChangeSetting].
7+
const ConfirmTaskStatusChangeSetting({super.key});
8+
9+
@override
10+
Widget build(final BuildContext context) => ListenableBuilder(
11+
listenable: SettingsHandler(),
12+
builder: (final context, final child) => SwitchListTile(
13+
contentPadding: EdgeInsets.zero,
14+
title: const Text("Confirm task status change"),
15+
subtitle: const Text(
16+
"Ask for confirmation before marking a task as done or reopening it.",
17+
),
18+
value: SettingsHandler().showMarkTaskStateConfirmation(),
19+
onChanged: (final value) =>
20+
SettingsHandler().setShowMarkTaskStateConfirmation(value: value),
21+
),
22+
);
23+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import "package:flutter/material.dart";
2+
import "package:gitdone/ui/settings/widgets/confirm_task_status_change_setting.dart";
3+
4+
/// A widget that allows users to manage general application settings.
5+
class GeneralSettings extends StatelessWidget {
6+
/// Creates an instance of [GeneralSettings].
7+
const GeneralSettings({super.key});
8+
9+
@override
10+
Widget build(final BuildContext context) => Column(
11+
crossAxisAlignment: CrossAxisAlignment.start,
12+
children: [
13+
Text("General Settings", style: Theme.of(context).textTheme.titleLarge),
14+
const ConfirmTaskStatusChangeSetting(),
15+
],
16+
);
17+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import "package:flutter/foundation.dart";
2+
import "package:flutter/material.dart";
3+
import "package:gitdone/core/task_handler.dart";
4+
import "package:gitdone/ui/_widgets/mark_task_confirmation_dialog.dart";
5+
import "package:gitdone/ui/task_details/task_details_view_model.dart";
6+
7+
/// Floating action buttons for the task details view.
8+
class TaskDetailsFloatingActionButton extends StatelessWidget {
9+
/// Creates a [TaskDetailsFloatingActionButton] widget.
10+
const TaskDetailsFloatingActionButton({required this.viewModel, super.key});
11+
12+
/// The view model for the task details.
13+
final TaskDetailsViewModel viewModel;
14+
15+
@override
16+
Widget build(final BuildContext context) {
17+
final _MarkTaskButtonConfig config = _markTaskButtonConfig();
18+
return Column(
19+
mainAxisSize: MainAxisSize.min,
20+
children: [
21+
FloatingActionButton.small(
22+
heroTag: "markTask",
23+
onPressed: () => showMarkTaskConfirmationDialog(
24+
context: context,
25+
currentTaskState: viewModel.task.state,
26+
onConfirm: config.onConfirm,
27+
),
28+
child: config.icon,
29+
),
30+
FloatingActionButton(
31+
heroTag: "editTask",
32+
onPressed: viewModel.editTask,
33+
child: const Icon(Icons.edit),
34+
),
35+
],
36+
);
37+
}
38+
39+
_MarkTaskButtonConfig _markTaskButtonConfig() =>
40+
viewModel.task.state == IssueState.open.value
41+
? (onConfirm: viewModel.markTaskAsDone, icon: const Icon(Icons.done))
42+
: (onConfirm: viewModel.markTaskAsOpen, icon: const Icon(Icons.undo));
43+
44+
@override
45+
void debugFillProperties(final DiagnosticPropertiesBuilder properties) {
46+
super.debugFillProperties(properties);
47+
properties.add(
48+
DiagnosticsProperty<TaskDetailsViewModel>("viewModel", viewModel),
49+
);
50+
}
51+
}
52+
53+
typedef _MarkTaskButtonConfig = ({VoidCallback onConfirm, Icon icon});

‎lib/ui/task_details/task_details_view.dart‎

Lines changed: 4 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import "package:flutter/foundation.dart";
22
import "package:flutter/material.dart";
33
import "package:gitdone/core/models/task.dart";
4-
import "package:gitdone/core/task_handler.dart";
54
import "package:gitdone/core/utils/navigation.dart";
65
import "package:gitdone/core/utils/snack_bar.dart";
76
import "package:gitdone/ui/_widgets/app_bar.dart";
87
import "package:gitdone/ui/_widgets/confirm_dialog.dart";
98
import "package:gitdone/ui/_widgets/page_title.dart";
109
import "package:gitdone/ui/_widgets/task_labels.dart";
10+
import "package:gitdone/ui/task_details/_widgets/task_details_floating_action_button.dart";
1111
import "package:gitdone/ui/task_details/task_details_view_model.dart";
1212
import "package:intl/intl.dart";
1313
import "package:markdown_widget/markdown_widget.dart";
@@ -53,7 +53,9 @@ class _TaskDetailsViewState extends State<TaskDetailsView> {
5353
],
5454
),
5555
),
56-
floatingActionButton: _renderFloatingActionButton(viewModel),
56+
floatingActionButton: TaskDetailsFloatingActionButton(
57+
viewModel: viewModel,
58+
),
5759
floatingActionButtonLocation: FloatingActionButtonLocation.endFloat,
5860
),
5961
),
@@ -98,27 +100,6 @@ class _TaskDetailsViewState extends State<TaskDetailsView> {
98100
),
99101
);
100102

101-
Widget _renderFloatingActionButton(final TaskDetailsViewModel viewModel) =>
102-
Column(
103-
mainAxisSize: MainAxisSize.min,
104-
children: [
105-
FloatingActionButton.small(
106-
heroTag: "markTask",
107-
onPressed: viewModel.task.state == IssueState.open.value
108-
? viewModel.markTaskAsDone
109-
: viewModel.markTaskAsOpen,
110-
child: viewModel.task.state == IssueState.open.value
111-
? const Icon(Icons.done)
112-
: const Icon(Icons.undo),
113-
),
114-
FloatingActionButton(
115-
heroTag: "editTask",
116-
onPressed: viewModel.editTask,
117-
child: const Icon(Icons.edit),
118-
),
119-
],
120-
);
121-
122103
MenuItemButton _deleteTaskButton(final TaskDetailsViewModel viewModel) =>
123104
MenuItemButton(
124105
onPressed: () => ConfirmDialog.show(

0 commit comments

Comments
 (0)