1
0
mirror of https://github.com/go-vikunja/app synced 2024-06-02 18:49:47 +00:00

buckets edit title, isDoneBucket, & limit; bucket deletion

This commit is contained in:
Paul Nettleton 2022-07-28 04:16:39 -05:00
parent ad30897bb3
commit 0762a7ae14
8 changed files with 622 additions and 371 deletions

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:vikunja_app/models/bucket.dart';
class BucketLimitDialog extends StatefulWidget {
final Bucket bucket;
const BucketLimitDialog({Key key, @required this.bucket}) : super(key: key);
@override
State<BucketLimitDialog> createState() => _BucketLimitDialogState();
}
class _BucketLimitDialogState extends State<BucketLimitDialog> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
if (_controller.text.isEmpty) _controller.text = '${widget.bucket.limit}';
return AlertDialog(
title: Text('Limit for ${widget.bucket.title}'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Expanded(
child: TextField(
controller: _controller,
decoration: InputDecoration(
labelText: 'Limit: ',
helperText: 'Set limit of 0 for no limit.',
),
keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
],
onSubmitted: (text) => Navigator.of(context).pop(int.parse(text)),
),
),
Column(
children: <Widget>[
IconButton(
onPressed: () => _controller.text = '${int.parse(_controller.text) + 1}',
icon: Icon(Icons.expand_less),
),
IconButton(
onPressed: () {
final limit = int.parse(_controller.text);
_controller.text = '${limit == 0 ? 0 : (limit - 1)}';
},
icon: Icon(Icons.expand_more),
),
],
),
],
),
],
),
actions: <TextButton>[
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(0),
child: Text('Remove Limit'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(int.parse(_controller.text)),
child: Text('Done'),
)
],
);
}
}

View File

@ -175,14 +175,17 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
),
),
),
onTap: () => Navigator.push<Task>(
context,
MaterialPageRoute(builder: (context) => TaskEditPage(
task: _currentTask,
)),
).then((task) => setState(() {
if (task != null) _currentTask = task;
})),
onTap: () {
FocusScope.of(context).unfocus();
Navigator.push<Task>(
context,
MaterialPageRoute(builder: (context) => TaskEditPage(
task: _currentTask,
)),
).then((task) => setState(() {
if (task != null) _currentTask = task;
}));
},
),
);
}

View File

@ -14,13 +14,14 @@ class Task {
String title, description;
bool done;
Color color;
double kanbanPosition;
User createdBy;
Duration repeatAfter;
List<Task> subtasks;
List<Label> labels;
List<TaskAttachment> attachments;
bool loading = false;
// TODO: add kanbanPosition, position(?)
// TODO: add position(?)
Task(
{@required this.id,
@ -35,6 +36,7 @@ class Task {
this.priority,
this.repeatAfter,
this.color,
this.kanbanPosition,
this.subtasks,
this.labels,
this.attachments,
@ -62,6 +64,9 @@ class Task {
color = json['hex_color'] == ''
? null
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
kanbanPosition = json['kanban_position'] is int
? json['kanban_position'].toDouble()
: json['kanban_position'],
labels = (json['labels'] as List<dynamic>)
?.map((label) => Label.fromJson(label))
?.cast<Label>()
@ -95,6 +100,7 @@ class Task {
'priority': priority,
'repeat_after': repeatAfter?.inSeconds,
'hex_color': color?.value?.toRadixString(16)?.padLeft(8, '0')?.substring(2),
'kanban_position': kanbanPosition,
'labels': labels?.map((label) => label.toJSON())?.toList(),
'subtasks': subtasks?.map((subtask) => subtask.toJSON())?.toList(),
'attachments': attachments?.map((attachment) => attachment.toJSON())?.toList(),
@ -116,6 +122,7 @@ class Task {
bool done,
Color color,
bool resetColor,
double kanbanPosition,
User createdBy,
Duration repeatAfter,
List<Task> subtasks,
@ -137,7 +144,8 @@ class Task {
title: title ?? this.title,
description: description ?? this.description,
done: done ?? this.done,
color: (resetColor ?? false) ? null : color ?? this.color,
color: (resetColor ?? false) ? null : (color ?? this.color),
kanbanPosition: kanbanPosition ?? this.kanbanPosition,
createdBy: createdBy ?? this.createdBy,
repeatAfter: repeatAfter ?? this.repeatAfter,
subtasks: subtasks ?? this.subtasks,

View File

@ -5,10 +5,12 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/TaskTile.dart';
import 'package:vikunja_app/components/SliverBucketList.dart';
import 'package:vikunja_app/components/SliverBucketPersistentHeader.dart';
import 'package:vikunja_app/components/BucketLimitDialog.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/task.dart';
@ -17,6 +19,16 @@ import 'package:vikunja_app/pages/list/list_edit.dart';
import 'package:vikunja_app/pages/list/task_edit.dart';
import 'package:vikunja_app/stores/list_store.dart';
enum BucketMenu {limit, done, delete}
class BucketProps {
final ValueKey<int> key;
bool scrollable = false;
final ScrollController controller = ScrollController();
final TextEditingController titleController = TextEditingController();
BucketProps(this.key);
}
class ListPage extends StatefulWidget {
final TaskList taskList;
@ -28,6 +40,7 @@ class ListPage extends StatefulWidget {
}
class _ListPageState extends State<ListPage> {
final _globalKey = GlobalKey();
int _viewIndex = 0;
TaskList _list;
List<Task> _loadingTasks = [];
@ -36,9 +49,7 @@ class _ListPageState extends State<ListPage> {
bool displayDoneTasks;
ListProvider taskState;
PageController _pageController;
Map<int, ValueKey<int>> _bucketKeys = {};
Map<int, bool> _bucketScrollable = {};
Map<int, ScrollController> _controllers = {};
Map<int, BucketProps> _bucketProps = {};
int _draggedBucketIndex;
@override
@ -57,74 +68,83 @@ class _ListPageState extends State<ListPage> {
@override
Widget build(BuildContext context) {
taskState = Provider.of<ListProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(_list.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
),
)).whenComplete(() => _loadList()),
),
],
),
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
? RefreshIndicator(
child: taskState.tasks.length > 0 || taskState.buckets.length > 0
? ListenableProvider.value(
value: taskState,
child: Theme(
data: (ThemeData base) {
return base.copyWith(
chipTheme: base.chipTheme.copyWith(
labelPadding: EdgeInsets.symmetric(horizontal: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
KeyboardVisibilityController().onChange.listen((visible) {
if (!visible) try {
FocusScope.of(_globalKey.currentContext ?? context).unfocus();
} catch (e) {}
});
return GestureDetector(
key: _globalKey,
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: Text(_list.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
),
)).whenComplete(() => _loadList()),
),
],
),
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
? RefreshIndicator(
child: taskState.tasks.length > 0 || taskState.buckets.length > 0
? ListenableProvider.value(
value: taskState,
child: Theme(
data: (ThemeData base) {
return base.copyWith(
chipTheme: base.chipTheme.copyWith(
labelPadding: EdgeInsets.symmetric(horizontal: 2),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)),
),
),
),
);
}(Theme.of(context)),
child: () {
switch (_viewIndex) {
case 0:
return _listView(context);
case 1:
return _kanbanView(context);
default:
return _listView(context);
}
}(),
),
)
: Center(child: Text('This list is empty.')),
onRefresh: _loadList,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _addItemDialog(context), child: Icon(Icons.add)),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.view_list),
label: 'List',
tooltip: 'List',
),
BottomNavigationBarItem(
icon: Icon(Icons.view_kanban),
label: 'Kanban',
tooltip: 'Kanban',
),
],
currentIndex: _viewIndex,
onTap: _onViewTapped,
);
}(Theme.of(context)),
child: () {
switch (_viewIndex) {
case 0:
return _listView(context);
case 1:
return _kanbanView(context);
default:
return _listView(context);
}
}(),
),
)
: Center(child: Text('This list is empty.')),
onRefresh: _loadList,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: _viewIndex == 1 ? null : Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _addItemDialog(context), child: Icon(Icons.add)),
),
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.view_list),
label: 'List',
tooltip: 'List',
),
BottomNavigationBarItem(
icon: Icon(Icons.view_kanban),
label: 'Kanban',
tooltip: 'Kanban',
),
],
currentIndex: _viewIndex,
onTap: _onViewTapped,
),
),
);
}
@ -177,6 +197,7 @@ class _ListPageState extends State<ListPage> {
scrollDirection: Axis.horizontal,
scrollController: _pageController,
physics: PageScrollPhysics(),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
itemCount: taskState.buckets.length,
itemExtent: bucketWidth,
cacheExtent: bucketWidth,
@ -278,22 +299,27 @@ class _ListPageState extends State<ListPage> {
final addTaskButton = ElevatedButton.icon(
icon: Icon(Icons.add),
label: Text('Add Task'),
onPressed: () => _addItemDialog(context, bucket),
onPressed: (bucket.tasks?.length ?? -1) < bucket.limit
? () => _addItemDialog(context, bucket)
: null,
);
if (_controllers[bucket.id] == null) {
_controllers[bucket.id] = ScrollController();
}
if (_bucketKeys[bucket.id] == null) {
if (_bucketKeys[bucket.id] == null)
_bucketKeys[bucket.id] = ValueKey<int>(bucket.id);
if (_bucketProps[bucket.id] == null) {
_bucketProps[bucket.id] = BucketProps(ValueKey<int>(bucket.id));
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
_bucketProps[bucket.id].scrollable = _bucketProps[bucket.id].controller.position.maxScrollExtent > 0;
});
});
}
if (_bucketProps[bucket.id].titleController.text.isEmpty)
_bucketProps[bucket.id].titleController.text = bucket.title;
return Stack(
key: _bucketKeys[bucket.id],
key: _bucketProps[bucket.id].key,
children: <Widget>[
CustomScrollView(
controller: _controllers[bucket.id],
controller: _bucketProps[bucket.id].controller,
slivers: <Widget>[
SliverBucketPersistentHeader(
minExtent: 56,
@ -301,11 +327,108 @@ class _ListPageState extends State<ListPage> {
child: Material(
color: theme.scaffoldBackgroundColor,
child: ListTile(
title: Text(
bucket.title,
style: theme.textTheme.titleLarge,
minLeadingWidth: 15,
horizontalTitleGap: 4,
leading: bucket.isDoneBucket ? Icon(
Icons.done_all,
color: Colors.green,
) : null,
title: Row(
children: <Widget>[
Expanded(
child: TextField(
controller: _bucketProps[bucket.id].titleController,
decoration: const InputDecoration.collapsed(
hintText: 'Bucket Title',
),
style: theme.textTheme.titleLarge,
onSubmitted: (title) {
if (title.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
'Bucket title cannot be empty!',
style: TextStyle(color: Colors.red),
),
));
return;
}
bucket.title = title;
_updateBucket(context, bucket);
},
),
),
if (bucket.limit != 0)
Text(
'${bucket.tasks?.length ?? 0}/${bucket.limit}',
style: theme.textTheme.titleMedium.copyWith(
color: (bucket.tasks?.length ?? -1) >= bucket.limit
? Colors.red : null,
),
),
],
),
trailing: PopupMenuButton<BucketMenu>(
child: Icon(Icons.more_vert),
onSelected: (item) {
switch (item) {
case BucketMenu.limit:
showDialog<int>(context: context,
builder: (_) => BucketLimitDialog(
bucket: bucket,
),
).then((limit) {
if (limit != null) {
bucket.limit = limit;
_updateBucket(context, bucket);
}
});
break;
case BucketMenu.done:
bucket.isDoneBucket = !bucket.isDoneBucket;
_updateBucket(context, bucket);
break;
case BucketMenu.delete:
_deleteBucket(context, bucket);
}
},
itemBuilder: (context) => <PopupMenuEntry<BucketMenu>>[
PopupMenuItem<BucketMenu>(
value: BucketMenu.limit,
child: Text('Limit: ${bucket.limit}'),
),
PopupMenuItem<BucketMenu>(
value: BucketMenu.done,
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 4),
child: Icon(
Icons.done_all,
color: bucket.isDoneBucket ? Colors.green : null,
),
),
Text('Done Bucket'),
],
),
),
const PopupMenuDivider(),
PopupMenuItem<BucketMenu>(
value: BucketMenu.delete,
child: Row(
children: <Widget>[
Icon(
Icons.delete,
color: Colors.red,
),
Text(
'Delete',
style: TextStyle(color: Colors.red),
),
],
),
),
],
),
trailing: Icon(Icons.more_vert),
),
),
),
@ -313,19 +436,10 @@ class _ListPageState extends State<ListPage> {
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverBucketList(
bucket: bucket,
onLast: () {
if (_bucketScrollable[bucket.id] == null) {
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
_bucketScrollable[bucket.id] = _controllers[bucket.id].position.maxScrollExtent > 0;
});
});
}
},
),
),
SliverVisibility(
visible: !(_bucketScrollable[bucket.id] ?? false),
visible: !_bucketProps[bucket.id].scrollable,
maintainState: true,
maintainAnimation: true,
maintainSize: true,
@ -339,7 +453,7 @@ class _ListPageState extends State<ListPage> {
),
],
),
if (_bucketScrollable[bucket.id] ?? false) Align(
if (_bucketProps[bucket.id].scrollable) Align(
alignment: Alignment.bottomCenter,
child: addTaskButton,
),
@ -444,6 +558,7 @@ class _ListPageState extends State<ListPage> {
}
_addBucketDialog(BuildContext context) {
FocusScope.of(context).unfocus();
showDialog(
context: context,
builder: (_) => AddDialog(
@ -478,6 +593,43 @@ class _ListPageState extends State<ListPage> {
await Provider.of<ListProvider>(context, listen: false).updateBucket(
context: context,
bucket: bucket,
);
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('${bucket.title} bucket updated successfully!'),
));
setState(() {});
});
}
_deleteBucket(BuildContext context, Bucket bucket) {
if ((bucket.tasks?.length ?? 0) > 0) {
int defaultBucketId = taskState.buckets[0]?.id ?? 100;
taskState.buckets.forEach((b) {
if (b.id < defaultBucketId)
defaultBucketId = b.id;
});
final defaultBucketIndex = taskState.buckets.indexWhere((b) => b.id == defaultBucketId);
bucket.tasks.forEach((task) {
taskState.buckets[defaultBucketIndex].tasks.add(task.copyWith(
bucketId: defaultBucketId,
kanbanPosition: taskState.buckets[defaultBucketIndex].tasks.last.kanbanPosition + 1.0,
));
});
}
Provider.of<ListProvider>(context, listen: false).deleteBucket(
context: context,
listId: bucket.listId,
bucketId: bucket.id,
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Row(
children: <Widget>[
Text('${bucket.title} was deleted.'),
Icon(Icons.delete),
],
),
));
setState(() {});
});
}
}

View File

@ -65,282 +65,285 @@ class _TaskEditPageState extends State<TaskEditPage> {
}
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(
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(),
child: GestureDetector(
onTap: () => FocusScope.of(context).unfocus(),
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(
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(),
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,
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,
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),
)
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 15, left: 2),
child: Icon(
Icons.palette,
color: Colors.grey,
),
),
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,
decoration: new InputDecoration(
labelText: 'Repeat after',
border: InputBorder.none,
icon: Icon(Icons.repeat),
),
),
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,
),
);
}(),
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,
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),
)
],
),
Padding(
padding: const EdgeInsets.only(top: 15),
child: Row(
children: <Widget>[
Padding(
padding: const EdgeInsets.only(right: 15, left: 2),
child: Icon(
Icons.palette,
color: Colors.grey,
),
),
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,
),
);
}(),
),
],
),
),
],
),
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: !_loading ? () {
if (_formKey.currentState.validate()) {
Form.of(_listKey.currentContext).save();
_saveTask(_listKey.currentContext);
}
} : null,
child: Icon(Icons.save),
floatingActionButton: FloatingActionButton(
onPressed: !_loading ? () {
if (_formKey.currentState.validate()) {
Form.of(_listKey.currentContext).save();
_saveTask(_listKey.currentContext);
}
} : null,
child: Icon(Icons.save),
),
),
),
);

View File

@ -98,7 +98,7 @@ class ListProvider with ChangeNotifier {
Future<void> addTask({BuildContext context, Task newTask, int listId}) {
var globalState = VikunjaGlobal.of(context);
_isLoading = true;
if (newTask.bucketId == null) _isLoading = true;
notifyListeners();
return globalState.taskService.add(listId, newTask).then((task) {
@ -137,12 +137,10 @@ class ListProvider with ChangeNotifier {
}
Future<void> addBucket({BuildContext context, Bucket newBucket, int listId}) {
_isLoading = true;
notifyListeners();
return VikunjaGlobal.of(context).bucketService.add(listId, newBucket)
.then((bucket) {
_buckets.add(bucket);
_isLoading = false;
notifyListeners();
});
}
@ -151,6 +149,15 @@ class ListProvider with ChangeNotifier {
return VikunjaGlobal.of(context).bucketService.update(bucket)
.then((rBucket) {
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
_buckets.sort((a, b) => a.position.compareTo(b.position));
notifyListeners();
});
}
Future<void> deleteBucket({BuildContext context, int listId, int bucketId}) {
return VikunjaGlobal.of(context).bucketService.delete(listId, bucketId)
.then((_) {
_buckets.removeWhere((bucket) => bucket.id == bucketId);
notifyListeners();
});
}

View File

@ -182,7 +182,7 @@ packages:
source: hosted
version: "1.0.3"
flutter_keyboard_visibility:
dependency: transitive
dependency: "direct main"
description:
name: flutter_keyboard_visibility
url: "https://pub.dartlang.org"

View File

@ -25,6 +25,7 @@ dependencies:
provider: ^6.0.3
webview_flutter: ^3.0.4
flutter_colorpicker: ^1.0.3
flutter_keyboard_visibility: ^5.3.0
dev_dependencies:
flutter_test: