edit task color, update edited task, some cleanup

This commit is contained in:
Paul Nettleton 2022-07-27 13:10:01 -05:00
parent 5d6120a7ac
commit c3a7962679
7 changed files with 403 additions and 258 deletions

View File

@ -174,12 +174,14 @@ class _BucketTaskCardState extends State<BucketTaskCard> {
),
),
),
onTap: () => Navigator.push(
onTap: () => Navigator.push<Task>(
context,
MaterialPageRoute(builder: (context) => TaskEditPage(
task: _currentTask,
)),
),
).then((task) => setState(() {
if (task != null) _currentTask = task;
})),
),
);
}

View File

@ -81,14 +81,16 @@ class TaskTileState extends State<TaskTile> {
: Text(_currentTask.description),
secondary:
IconButton(icon: Icon(Icons.settings), onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => TaskEditPage(
task: _currentTask,
))).whenComplete(() {
widget.onEdit();
});
Navigator.push<Task>(
context,
MaterialPageRoute(
builder: (context) => TaskEditPage(
task: _currentTask,
),
),
).then((task) => setState(() {
if (task != null) _currentTask = task;
})).whenComplete(() => widget.onEdit());
}),
onChanged: _change,
);
@ -107,15 +109,9 @@ class TaskTileState extends State<TaskTile> {
}
Future<Task> _updateTask(Task task, bool checked) {
// TODO use copyFrom
return VikunjaGlobal.of(context).taskService.update(Task(
id: task.id,
done: checked,
title: task.title,
description: task.description,
createdBy: task.createdBy,
dueDate: task.dueDate
));
return VikunjaGlobal.of(context).taskService.update(task.copyWith(
done: checked,
));
}
}

View File

@ -107,4 +107,42 @@ class Task {
Color get textColor => color != null
? color.computeLuminance() > 0.5 ? Colors.black : Colors.white
: null;
Task copyWith({
int id, int parentTaskId, int priority, int listId, int bucketId,
DateTime created, DateTime updated, DateTime dueDate, DateTime startDate, DateTime endDate,
List<DateTime> reminderDates,
String title, String description,
bool done,
Color color,
bool resetColor,
User createdBy,
Duration repeatAfter,
List<Task> subtasks,
List<Label> labels,
List<TaskAttachment> attachments,
}) {
return Task(
id: id ?? this.id,
parentTaskId: parentTaskId ?? this.parentTaskId,
priority: priority ?? this.priority,
listId: listId ?? this.listId,
bucketId: bucketId ?? this.bucketId,
created: created ?? this.created,
updated: updated ?? this.updated,
dueDate: dueDate ?? this.dueDate,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
reminderDates: reminderDates ?? this.reminderDates,
title: title ?? this.title,
description: description ?? this.description,
done: done ?? this.done,
color: (resetColor ?? false) ? null : color ?? this.color,
createdBy: createdBy ?? this.createdBy,
repeatAfter: repeatAfter ?? this.repeatAfter,
subtasks: subtasks ?? this.subtasks,
labels: labels ?? this.labels,
attachments: attachments ?? this.attachments,
);
}
}

View File

@ -224,14 +224,16 @@ class _ListPageState extends State<ListPage> {
onReorderStart: (oldIndex) => setState(() => _draggedBucketIndex = oldIndex),
onReorder: (oldIndex, newIndex) {},
onReorderEnd: (newIndex) => setState(() {
if (newIndex > _draggedBucketIndex) newIndex -= 1;
taskState.buckets.insert(newIndex, taskState.buckets.removeAt(_draggedBucketIndex));
bool indexUpdated = false;
if (newIndex > _draggedBucketIndex) {
newIndex -= 1;
indexUpdated = true;
}
taskState.buckets.insert(newIndex, taskState.buckets.removeAt(_draggedBucketIndex));
if (newIndex == 0) {
taskState.buckets[0].position = 0;
_updateBucket(context, taskState.buckets[0]);
newIndex = 1;
indexUpdated = true;
}
taskState.buckets[newIndex].position = newIndex == taskState.buckets.length - 1
? taskState.buckets[newIndex - 1].position + 1
@ -239,7 +241,9 @@ class _ListPageState extends State<ListPage> {
+ taskState.buckets[newIndex + 1].position) / 2.0;
_updateBucket(context, taskState.buckets[newIndex]);
_draggedBucketIndex = null;
_pageController.jumpToPage(indexUpdated ? 0 : newIndex);
if (indexUpdated)
_pageController.jumpToPage((newIndex
/ (deviceData.orientation == Orientation.portrait ? 1 : 2)).floor());
}),
);
}

View File

@ -2,13 +2,12 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:vikunja_app/components/datetimePicker.dart';
import 'package:vikunja_app/components/label.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
import 'package:vikunja_app/utils/repeat_after_parse.dart';
class TaskEditPage extends StatefulWidget {
@ -22,6 +21,7 @@ class TaskEditPage extends StatefulWidget {
class _TaskEditPageState extends State<TaskEditPage> {
final _formKey = GlobalKey<FormState>();
final _listKey = GlobalKey();
bool _loading = false;
bool _changed = false;
@ -35,6 +35,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
List<Label> _suggestedLabels;
var _reminderInputs = <Widget>[];
final _labelTypeAheadController = TextEditingController();
Color _color;
Color _pickerColor;
bool _resetColor = false;
@override
Widget build(BuildContext ctx) {
@ -44,282 +47,322 @@ class _TaskEditPageState extends State<TaskEditPage> {
_reminderDates?.asMap()?.forEach((i, time) =>
setState(() => _reminderInputs?.add(VikunjaDateTimePicker(
initialValue: time,
label: 'Reminder',
onSaved: (reminder) => _reminderDates[i] = reminder,
))));
initialValue: time,
label: 'Reminder',
onSaved: (reminder) => _reminderDates[i] = reminder,
)))
);
}
if (_labels == null) {
_labels = widget.task.labels ?? [];
}
return WillPopScope(onWillPop: () {
if(_changed) {
return _showConfirmationDialog();
}
return new Future(() => true);
},
child: Scaffold(
appBar: AppBar(
title: Text('Edit Task'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
child: Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(16.0), children: <
Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.task.title,
onSaved: (title) => _title = title,
onChanged: (_) => _changed = true,
validator: (title) {
if (title.length < 3 || title.length > 250) {
return 'The title needs to have between 3 and 250 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Title',
border: OutlineInputBorder(),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.task.description,
onSaved: (description) => _description = description,
onChanged: (_) => _changed = true,
validator: (description) {
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
),
),
VikunjaDateTimePicker(
icon: Icon(Icons.access_time),
label: 'Due Date',
initialValue: widget.task.dueDate,
onSaved: (duedate) => _dueDate = duedate,
onChanged: (_) => _changed = true,
),
VikunjaDateTimePicker(
label: 'Start Date',
initialValue: widget.task.startDate,
onSaved: (startDate) => _startDate = startDate,
onChanged: (_) => _changed = true,
),
VikunjaDateTimePicker(
label: 'End Date',
initialValue: widget.task.endDate,
onSaved: (endDate) => _endDate = endDate,
onChanged: (_) => _changed = true,
),
Row(
children: [
Expanded(
flex: 2,
return WillPopScope(
onWillPop: () {
if(_changed) {
return _showConfirmationDialog();
}
return new Future(() => true);
},
child: Scaffold(
appBar: AppBar(
title: Text('Edit Task'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
child: Form(
key: _formKey,
child: ListView(
key: _listKey,
padding: const EdgeInsets.all(16.0),
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
keyboardType: TextInputType.number,
initialValue: getRepeatAfterValueFromDuration(
widget.task.repeatAfter)
?.toString(),
onSaved: (repeatAfter) => _repeatAfter =
getDurationFromType(repeatAfter, _repeatAfterType),
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.task.title,
onSaved: (title) => _title = title,
onChanged: (_) => _changed = true,
validator: (title) {
if (title.length < 3 || title.length > 250) {
return 'The title needs to have between 3 and 250 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Repeat after',
border: InputBorder.none,
icon: Icon(Icons.repeat),
labelText: 'Title',
border: OutlineInputBorder(),
),
),
),
Expanded(
child: DropdownButton<String>(
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.task.description,
onSaved: (description) => _description = description,
onChanged: (_) => _changed = true,
validator: (description) {
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
),
),
VikunjaDateTimePicker(
icon: Icon(Icons.access_time),
label: 'Due Date',
initialValue: widget.task.dueDate,
onSaved: (duedate) => _dueDate = duedate,
onChanged: (_) => _changed = true,
),
VikunjaDateTimePicker(
label: 'Start Date',
initialValue: widget.task.startDate,
onSaved: (startDate) => _startDate = startDate,
onChanged: (_) => _changed = true,
),
VikunjaDateTimePicker(
label: 'End Date',
initialValue: widget.task.endDate,
onSaved: (endDate) => _endDate = endDate,
onChanged: (_) => _changed = true,
),
Row(
children: [
Expanded(
flex: 2,
child: TextFormField(
keyboardType: TextInputType.number,
initialValue: getRepeatAfterValueFromDuration(
widget.task.repeatAfter)?.toString(),
onSaved: (repeatAfter) => _repeatAfter =
getDurationFromType(repeatAfter, _repeatAfterType),
onChanged: (_) => _changed = true,
decoration: new InputDecoration(
labelText: 'Repeat after',
border: InputBorder.none,
icon: Icon(Icons.repeat),
),
),
),
Expanded(
child: DropdownButton<String>(
isExpanded: true,
isDense: true,
value: _repeatAfterType ??
getRepeatAfterTypeFromDuration(
widget.task.repeatAfter),
onChanged: (String newValue) {
setState(() {
_repeatAfterType = newValue;
});
},
items: <String>[
'Hours',
'Days',
'Weeks',
'Months',
'Years'
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
value: value,
child: Text(value),
);
}).toList(),
),
),
],
),
Column(
children: _reminderInputs,
),
GestureDetector(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10),
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 15, left: 2),
child: Icon(
Icons.alarm_add,
color: Colors.grey,
)),
Text(
'Add a reminder',
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
],
),
),
onTap: () {
// We add a new entry every time we add a new input, to make sure all inputs have a place where they can put their value.
_reminderDates.add(null);
var currentIndex = _reminderDates.length - 1;
// FIXME: Why does putting this into a row fails?
setState(() => _reminderInputs.add(
VikunjaDateTimePicker(
label: 'Reminder',
onSaved: (reminder) =>
_reminderDates[currentIndex] = reminder,
onChanged: (_) => _changed = true,
initialValue: DateTime.now(),
),
));
}),
InputDecorator(
isEmpty: _priority == null,
decoration: InputDecoration(
icon: const Icon(Icons.flag),
labelText: 'Priority',
border: InputBorder.none,
),
child: new DropdownButton<String>(
value: _priorityToString(_priority),
isExpanded: true,
isDense: true,
value: _repeatAfterType ??
getRepeatAfterTypeFromDuration(
widget.task.repeatAfter),
onChanged: (String newValue) {
setState(() {
_repeatAfterType = newValue;
_priority = _priorityFromString(newValue);
});
},
items: <String>[
'Hours',
'Days',
'Weeks',
'Months',
'Years'
].map<DropdownMenuItem<String>>((String value) {
return DropdownMenuItem<String>(
items: ['Unset', 'Low', 'Medium', 'High', 'Urgent', 'DO NOW']
.map((String value) {
return new DropdownMenuItem(
value: value,
child: Text(value),
child: new Text(value),
);
}).toList(),
),
),
],
),
Column(
children: _reminderInputs,
),
GestureDetector(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 10),
Wrap(
spacing: 10,
children: _labels.map((Label label) {
return LabelComponent(
label: label,
onDelete: () {
_removeLabel(label);
},
);
}).toList()),
Row(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width - 80,
child: TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _labelTypeAheadController,
decoration:
InputDecoration(labelText: 'Add a new label')),
suggestionsCallback: (pattern) => _searchLabel(pattern),
itemBuilder: (context, suggestion) {
return Text(suggestion);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (suggestion) {
_addLabel(suggestion);
},
),
),
IconButton(
onPressed: () =>
_createAndAddLabel(_labelTypeAheadController.text),
icon: Icon(Icons.add),
)
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
children: <Widget>[
Padding(
padding: EdgeInsets.only(right: 15, left: 2),
child: Icon(
Icons.alarm_add,
color: Colors.grey,
)),
Text(
'Add a reminder',
style: TextStyle(
padding: const EdgeInsets.only(right: 15, left: 2),
child: Icon(
Icons.palette,
color: Colors.grey,
fontSize: 16,
),
),
ElevatedButton(
child: Text(
'Color',
style: _resetColor || (_color ?? widget.task.color) == null ? null : TextStyle(
color: (_color ?? widget.task.color)
.computeLuminance() > 0.5 ? Colors.black : Colors.white,
),
),
style: _resetColor ? null : ButtonStyle(
backgroundColor: MaterialStateProperty
.resolveWith((_) => _color ?? widget.task.color),
),
onPressed: _onColorEdit,
),
Padding(
padding: const EdgeInsets.only(left: 15),
child: () {
String colorString = (_resetColor ? null : (_color ?? widget.task.color))?.toString();
colorString = colorString?.substring(10, colorString.length - 1)?.toUpperCase();
colorString = colorString != null ? '#$colorString' : 'None';
return Text(
'$colorString',
style: TextStyle(
color: Colors.grey,
fontStyle: FontStyle.italic,
),
);
}(),
),
],
),
),
onTap: () {
// We add a new entry every time we add a new input, to make sure all inputs have a place where they can put their value.
_reminderDates.add(null);
var currentIndex = _reminderDates.length - 1;
// FIXME: Why does putting this into a row fails?
setState(() => _reminderInputs.add(
VikunjaDateTimePicker(
label: 'Reminder',
onSaved: (reminder) =>
_reminderDates[currentIndex] = reminder,
onChanged: (_) => _changed = true,
initialValue: DateTime.now(),
),
));
}),
InputDecorator(
isEmpty: _priority == null,
decoration: InputDecoration(
icon: const Icon(Icons.flag),
labelText: 'Priority',
border: InputBorder.none,
),
child: new DropdownButton<String>(
value: _priorityToString(_priority),
isExpanded: true,
isDense: true,
onChanged: (String newValue) {
setState(() {
_priority = _priorityFromString(newValue);
});
},
items: ['Unset', 'Low', 'Medium', 'High', 'Urgent', 'DO NOW']
.map((String value) {
return new DropdownMenuItem(
value: value,
child: new Text(value),
);
}).toList(),
),
),
Wrap(
spacing: 10,
children: _labels.map((Label label) {
return LabelComponent(
label: label,
onDelete: () {
_removeLabel(label);
},
);
}).toList()),
Row(
children: <Widget>[
Container(
width: MediaQuery.of(context).size.width - 80,
child: TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _labelTypeAheadController,
decoration:
InputDecoration(labelText: 'Add a new label')),
suggestionsCallback: (pattern) => _searchLabel(pattern),
itemBuilder: (context, suggestion) {
return Text(suggestion);
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (suggestion) {
_addLabel(suggestion);
},
),
),
IconButton(
onPressed: () =>
_createAndAddLabel(_labelTypeAheadController.text),
icon: Icon(Icons.add),
)
],
),
Builder(
builder: (context) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_saveTask(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),
))),
]),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: !_loading ? () {
if (_formKey.currentState.validate()) {
Form.of(_listKey.currentContext).save();
_saveTask(_listKey.currentContext);
}
} : null,
child: Icon(Icons.save),
),
),
));
);
}
_saveTask(BuildContext context) async {
_saveTask(BuildContext context) async {
setState(() => _loading = true);
// Removes all reminders with no value set.
_reminderDates.removeWhere((d) => d == null);
Task updatedTask = Task(
id: widget.task.id,
Task updatedTask = widget.task.copyWith(
title: _title,
description: _description,
done: widget.task.done,
reminderDates: _reminderDates,
createdBy: widget.task.createdBy,
dueDate: _dueDate,
startDate: _startDate,
endDate: _endDate,
priority: _priority,
repeatAfter: _repeatAfter,
color: _resetColor ? null : (_color ?? widget.task.color),
resetColor: _resetColor,
);
// update the labels
@ -335,11 +378,12 @@ class _TaskEditPageState extends State<TaskEditPage> {
);
});
VikunjaGlobal.of(context).taskService.update(updatedTask).then((_) {
VikunjaGlobal.of(context).taskService.update(updatedTask).then((result) {
setState(() { _loading = false; _changed = false;});
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The task was updated successfully!'),
));
Navigator.of(context).pop(result);
}).catchError((err) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
@ -361,15 +405,13 @@ class _TaskEditPageState extends State<TaskEditPage> {
_searchLabel(String query) {
return VikunjaGlobal.of(context)
.labelService
.getAll(query: query)
.then((labels) {
.labelService.getAll(query: query).then((labels) {
log("searched");
// Only show those labels which aren't already added to the task
labels.removeWhere((labelToRemove) => _labels.contains(labelToRemove));
_suggestedLabels = labels;
return labels.map((label) => label.title).toList();
});
// Only show those labels which aren't already added to the task
labels.removeWhere((labelToRemove) => _labels.contains(labelToRemove));
_suggestedLabels = labels;
return labels.map((label) => label.title).toList();
});
}
_addLabel(String labelTitle) {
@ -444,6 +486,61 @@ class _TaskEditPageState extends State<TaskEditPage> {
}
}
_onColorEdit() {
_pickerColor = _resetColor || (_color ?? widget.task.color) == null
? Colors.black
: _color ?? widget.task.color;
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Task Color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: _pickerColor,
enableAlpha: false,
labelTypes: const [ColorLabelType.hsl, ColorLabelType.rgb],
paletteType: PaletteType.hslWithLightness,
hexInputBar: true,
onColorChanged: (color) => setState(() => _pickerColor = color),
),
),
actions: <TextButton>[
TextButton(
child: Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: Text('Reset'),
onPressed: () {
setState(() {
_color = null;
_resetColor = true;
_changed = _color != widget.task.color;
});
Navigator.of(context).pop();
},
),
TextButton(
child: Text('Ok'),
onPressed: () {
if (_pickerColor != Colors.black) setState(() {
_color = _pickerColor;
_resetColor = false;
_changed = _color != widget.task.color;
});
else setState(() {
_color = null;
_resetColor = true;
_changed = _color != widget.task.color;
});
Navigator.of(context).pop();
},
),
],
),
);
}
Future<bool> _showConfirmationDialog() async {
return showDialog<bool>(
context: context,

View File

@ -174,6 +174,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_colorpicker:
dependency: "direct main"
description:
name: flutter_colorpicker
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
flutter_keyboard_visibility:
dependency: transitive
description:

View File

@ -24,6 +24,7 @@ dependencies:
petitparser: ^5.0.0
provider: ^6.0.3
webview_flutter: ^3.0.4
flutter_colorpicker: ^1.0.3
dev_dependencies:
flutter_test: