null-safety & some other cleanup
This commit is contained in:
parent
9161e1fa12
commit
d4f234d65c
|
@ -4,7 +4,11 @@
|
||||||
"name": "Flutter",
|
"name": "Flutter",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"type": "dart",
|
"type": "dart",
|
||||||
"flutterMode": "debug"
|
"flutterMode": "debug",
|
||||||
|
"args": [
|
||||||
|
"--flavor",
|
||||||
|
"main"
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,13 @@ class Client {
|
||||||
GlobalKey<ScaffoldMessengerState> global;
|
GlobalKey<ScaffoldMessengerState> global;
|
||||||
final JsonDecoder _decoder = new JsonDecoder();
|
final JsonDecoder _decoder = new JsonDecoder();
|
||||||
final JsonEncoder _encoder = new JsonEncoder();
|
final JsonEncoder _encoder = new JsonEncoder();
|
||||||
String? _token;
|
String _token = '';
|
||||||
String? _base;
|
String _base = '';
|
||||||
bool authenticated = false;
|
bool authenticated = false;
|
||||||
bool ignoreCertificates = false;
|
bool ignoreCertificates = false;
|
||||||
|
|
||||||
String? get base => _base;
|
String get base => _base;
|
||||||
String? get token => _token;
|
String get token => _token;
|
||||||
|
|
||||||
String? post_body;
|
String? post_body;
|
||||||
|
|
||||||
|
@ -45,7 +45,7 @@ class Client {
|
||||||
|
|
||||||
get _headers =>
|
get _headers =>
|
||||||
{
|
{
|
||||||
'Authorization': _token != null ? 'Bearer $_token' : '',
|
'Authorization': _token != '' ? 'Bearer $_token' : '',
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -63,26 +63,14 @@ class Client {
|
||||||
|
|
||||||
|
|
||||||
void reset() {
|
void reset() {
|
||||||
_token = _base = null;
|
_token = _base = '';
|
||||||
authenticated = false;
|
authenticated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Response> get(String url,
|
Future<Response> get(String url,
|
||||||
[Map<String, List<String>>? queryParameters]) {
|
[Map<String, List<String>>? queryParameters]) {
|
||||||
// TODO: This could be moved to a seperate function
|
final uri = Uri.parse('${this.base}$url').replace(queryParameters: queryParameters);
|
||||||
var uri = Uri.parse('${this.base}$url');
|
return http.get(uri, headers: _headers)
|
||||||
// Because these are all final values, we can't just add the queryParameters and must instead build a new Uri Object every time this method is called.
|
|
||||||
var newUri = Uri(
|
|
||||||
scheme: uri.scheme,
|
|
||||||
userInfo: uri.userInfo,
|
|
||||||
host: uri.host,
|
|
||||||
port: uri.port,
|
|
||||||
path: uri.path,
|
|
||||||
query: uri.query,
|
|
||||||
queryParameters: queryParameters,
|
|
||||||
// Because dart takes a Map<String, String> here, it is only possible to sort by one parameter while the api supports n parameters.
|
|
||||||
fragment: uri.fragment);
|
|
||||||
return http.get(newUri, headers: _headers)
|
|
||||||
.then(_handleResponse, onError: _handleError);
|
.then(_handleResponse, onError: _handleError);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,8 @@ class LabelAPIService extends APIService implements LabelService {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Label>> getAll({String? query}) {
|
Future<List<Label>> getAll({String? query}) {
|
||||||
String? params =
|
String params =
|
||||||
query == null ? null : '?s=' + Uri.encodeQueryComponent(query);
|
query == null ? '' : '?s=' + Uri.encodeQueryComponent(query);
|
||||||
return client.get('/labels$params').then(
|
return client.get('/labels$params').then(
|
||||||
(response) => convertList(response.body, (result) => Label.fromJson(result)));
|
(response) => convertList(response.body, (result) => Label.fromJson(result)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:vikunja_app/components/datetimePicker.dart';
|
import 'package:vikunja_app/components/datetimePicker.dart';
|
||||||
|
import 'package:vikunja_app/global.dart';
|
||||||
import 'dart:developer';
|
import 'dart:developer';
|
||||||
import '../models/task.dart';
|
import '../models/task.dart';
|
||||||
|
|
||||||
enum NewTaskDue {day,week, month, custom}
|
enum NewTaskDue {day,week, month, custom}
|
||||||
|
// TODO: add to enum above
|
||||||
Map<NewTaskDue, Duration> newTaskDueToDuration = {
|
Map<NewTaskDue, Duration> newTaskDueToDuration = {
|
||||||
NewTaskDue.day: Duration(days: 1),
|
NewTaskDue.day: Duration(days: 1),
|
||||||
NewTaskDue.week: Duration(days: 7),
|
NewTaskDue.week: Duration(days: 7),
|
||||||
|
@ -12,7 +14,7 @@ Map<NewTaskDue, Duration> newTaskDueToDuration = {
|
||||||
|
|
||||||
class AddDialog extends StatefulWidget {
|
class AddDialog extends StatefulWidget {
|
||||||
final ValueChanged<String>? onAdd;
|
final ValueChanged<String>? onAdd;
|
||||||
final ValueChanged<Task>? onAddTask;
|
final void Function(String title, DateTime? dueDate)? onAddTask;
|
||||||
final InputDecoration? decoration;
|
final InputDecoration? decoration;
|
||||||
const AddDialog({Key? key, this.onAdd, this.decoration, this.onAddTask}) : super(key: key);
|
const AddDialog({Key? key, this.onAdd, this.decoration, this.onAddTask}) : super(key: key);
|
||||||
|
|
||||||
|
@ -65,11 +67,7 @@ class AddDialogState extends State<AddDialog> {
|
||||||
if (widget.onAdd != null && textController.text.isNotEmpty)
|
if (widget.onAdd != null && textController.text.isNotEmpty)
|
||||||
widget.onAdd!(textController.text);
|
widget.onAdd!(textController.text);
|
||||||
if(widget.onAddTask != null && textController.text.isNotEmpty) {
|
if(widget.onAddTask != null && textController.text.isNotEmpty) {
|
||||||
widget.onAddTask!(Task(id: 0,
|
widget.onAddTask!(textController.text, customDueDate);
|
||||||
title: textController.text,
|
|
||||||
done: false,
|
|
||||||
createdBy: null,
|
|
||||||
dueDate: customDueDate, identifier: ''));
|
|
||||||
}
|
}
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,11 +30,7 @@ class BucketTaskCard extends StatefulWidget {
|
||||||
required this.index,
|
required this.index,
|
||||||
required this.onDragUpdate,
|
required this.onDragUpdate,
|
||||||
required this.onAccept,
|
required this.onAccept,
|
||||||
}) : assert(task != null),
|
}) : super(key: key);
|
||||||
assert(index != null),
|
|
||||||
assert(onDragUpdate != null),
|
|
||||||
assert(onAccept != null),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<BucketTaskCard> createState() => _BucketTaskCardState();
|
State<BucketTaskCard> createState() => _BucketTaskCardState();
|
||||||
|
@ -61,9 +57,9 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
final identifierRow = Row(
|
final identifierRow = Row(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Text(
|
Text(
|
||||||
widget.task.identifier.isNotEmpty
|
(widget.task.identifier?.isNotEmpty ?? false)
|
||||||
? '#${widget.task.identifier.substring(1)}' : '${widget.task.id}',
|
? '#${widget.task.identifier!.substring(1)}' : '${widget.task.id}',
|
||||||
style: theme.textTheme.subtitle2?.copyWith(
|
style: (theme.textTheme.subtitle2 ?? TextStyle()).copyWith(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -76,7 +72,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
child: FittedBox(
|
child: FittedBox(
|
||||||
child: Chip(
|
child: Chip(
|
||||||
label: Text('Done'),
|
label: Text('Done'),
|
||||||
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
labelStyle: (theme.textTheme.labelLarge ?? TextStyle()).copyWith(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: theme.brightness == Brightness.dark
|
color: theme.brightness == Brightness.dark
|
||||||
? Colors.black : Colors.white,
|
? Colors.black : Colors.white,
|
||||||
|
@ -91,8 +87,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.task.title ?? "",
|
widget.task.title,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16)).copyWith(
|
||||||
color: widget.task.textColor,
|
color: widget.task.textColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -112,7 +108,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
color: pastDue ? Colors.red : null,
|
color: pastDue ? Colors.red : null,
|
||||||
),
|
),
|
||||||
label: Text(durationToHumanReadable(duration)),
|
label: Text(durationToHumanReadable(duration)),
|
||||||
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
labelStyle: (theme.textTheme.labelLarge ?? TextStyle()).copyWith(
|
||||||
color: pastDue ? Colors.red : null,
|
color: pastDue ? Colors.red : null,
|
||||||
),
|
),
|
||||||
backgroundColor: pastDue ? Colors.red.withAlpha(20) : null,
|
backgroundColor: pastDue ? Colors.red.withAlpha(20) : null,
|
||||||
|
@ -126,10 +122,10 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
spacing: 4,
|
spacing: 4,
|
||||||
runSpacing: 4,
|
runSpacing: 4,
|
||||||
);
|
);
|
||||||
widget.task.labels?.sort((a, b) => a.title?.compareTo(b.title ?? "") ?? 0);
|
widget.task.labels.sort((a, b) => a.title.compareTo(b.title));
|
||||||
widget.task.labels?.asMap().forEach((i, label) {
|
widget.task.labels.asMap().forEach((i, label) {
|
||||||
labelRow.children.add(Chip(
|
labelRow.children.add(Chip(
|
||||||
label: Text(label.title ?? ""),
|
label: Text(label.title),
|
||||||
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
labelStyle: theme.textTheme.labelLarge?.copyWith(
|
||||||
color: label.textColor,
|
color: label.textColor,
|
||||||
),
|
),
|
||||||
|
@ -137,7 +133,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
if (widget.task.hasCheckboxes) {
|
if (widget.task.hasCheckboxes) {
|
||||||
final checkboxStatistics = widget.task.checkboxStatistics!;
|
final checkboxStatistics = widget.task.checkboxStatistics;
|
||||||
final iconSize = (theme.textTheme.labelLarge?.fontSize ?? 14) + 2;
|
final iconSize = (theme.textTheme.labelLarge?.fontSize ?? 14) + 2;
|
||||||
labelRow.children.add(Chip(
|
labelRow.children.add(Chip(
|
||||||
avatar: Container(
|
avatar: Container(
|
||||||
|
@ -153,7 +149,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (widget.task.attachments != null && widget.task.attachments!.isNotEmpty) {
|
if (widget.task.attachments.isNotEmpty) {
|
||||||
labelRow.children.add(Chip(
|
labelRow.children.add(Chip(
|
||||||
label: Transform.rotate(
|
label: Transform.rotate(
|
||||||
angle: -pi / 4.0,
|
angle: -pi / 4.0,
|
||||||
|
@ -161,7 +157,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if (widget.task.description != null && widget.task.description!.isNotEmpty) {
|
if (widget.task.description.isNotEmpty) {
|
||||||
labelRow.children.add(Chip(
|
labelRow.children.add(Chip(
|
||||||
label: Icon(Icons.notes),
|
label: Icon(Icons.notes),
|
||||||
));
|
));
|
||||||
|
@ -237,7 +233,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
child: () {
|
child: () {
|
||||||
if (_dragging || _cardSize == null) return card;
|
if (_dragging || _cardSize == null) return card;
|
||||||
|
|
||||||
final dropBoxSize = _dropData?.size ?? _cardSize;
|
final cardSize = _cardSize!;
|
||||||
|
final dropBoxSize = _dropData?.size ?? cardSize;
|
||||||
final dropBox = DottedBorder(
|
final dropBox = DottedBorder(
|
||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
child: SizedBox.fromSize(size: dropBoxSize),
|
child: SizedBox.fromSize(size: dropBoxSize),
|
||||||
|
@ -268,8 +265,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
};
|
};
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: _cardSize!.width,
|
width: cardSize.width,
|
||||||
height: _cardSize!.height + (dropAbove || dropBelow ? dropBoxSize!.height + 4 : 0),
|
height: cardSize.height + (dropAbove || dropBelow ? dropBoxSize.height + 4 : 0),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
Column(
|
Column(
|
||||||
|
@ -282,7 +279,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
Column(
|
Column(
|
||||||
children: <SizedBox>[
|
children: <SizedBox>[
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: (_cardSize!.height / 2) + (dropAbove ? dropBoxSize!.height : 0),
|
height: (cardSize.height / 2) + (dropAbove ? dropBoxSize.height : 0),
|
||||||
child: DragTarget<TaskData>(
|
child: DragTarget<TaskData>(
|
||||||
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.above),
|
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.above),
|
||||||
onAccept: dragTargetOnAccept,
|
onAccept: dragTargetOnAccept,
|
||||||
|
@ -291,7 +288,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: (_cardSize!.height / 2) + (dropBelow ? dropBoxSize!.height : 0),
|
height: (cardSize.height / 2) + (dropBelow ? dropBoxSize.height : 0),
|
||||||
child: DragTarget<TaskData>(
|
child: DragTarget<TaskData>(
|
||||||
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.below),
|
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.below),
|
||||||
onAccept: dragTargetOnAccept,
|
onAccept: dragTargetOnAccept,
|
||||||
|
|
|
@ -19,7 +19,6 @@ class SliverBucketList extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
if (bucket.tasks == null) return null;
|
|
||||||
return index >= bucket.tasks.length ? null : BucketTaskCard(
|
return index >= bucket.tasks.length ? null : BucketTaskCard(
|
||||||
key: ObjectKey(bucket.tasks[index]),
|
key: ObjectKey(bucket.tasks[index]),
|
||||||
task: bucket.tasks[index],
|
task: bucket.tasks[index],
|
||||||
|
@ -33,14 +32,16 @@ class SliverBucketList extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) {
|
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) async {
|
||||||
return Provider.of<ListProvider>(context, listen: false).moveTaskToBucket(
|
await Provider.of<ListProvider>(context, listen: false).moveTaskToBucket(
|
||||||
context: context,
|
context: context,
|
||||||
task: task,
|
task: task,
|
||||||
newBucketId: bucket.id,
|
newBucketId: bucket.id,
|
||||||
index: index,
|
index: index,
|
||||||
).then((_) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
content: Text('${task.title} was moved to ${bucket.title} successfully!'),
|
content: Text('${task.title} was moved to ${bucket.title} successfully!'),
|
||||||
)));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,9 +14,14 @@ class TaskTile extends StatefulWidget {
|
||||||
final bool loading;
|
final bool loading;
|
||||||
final ValueSetter<bool>? onMarkedAsDone;
|
final ValueSetter<bool>? onMarkedAsDone;
|
||||||
|
|
||||||
const TaskTile(
|
const TaskTile({
|
||||||
{Key? key, required this.task, required this.onEdit, this.loading = false, this.showInfo = false, this.onMarkedAsDone})
|
Key? key,
|
||||||
: super(key: key);
|
required this.task,
|
||||||
|
required this.onEdit,
|
||||||
|
this.loading = false,
|
||||||
|
this.showInfo = false,
|
||||||
|
this.onMarkedAsDone,
|
||||||
|
}) : super(key: key);
|
||||||
/*
|
/*
|
||||||
@override
|
@override
|
||||||
TaskTileState createState() {
|
TaskTileState createState() {
|
||||||
|
@ -49,11 +54,11 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
|
||||||
strokeWidth: 2.0,
|
strokeWidth: 2.0,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
title: Text(_currentTask.title ?? ""),
|
title: Text(_currentTask.title),
|
||||||
subtitle:
|
subtitle:
|
||||||
_currentTask.description == null || _currentTask.description!.isEmpty
|
_currentTask.description.isEmpty
|
||||||
? null
|
? null
|
||||||
: Text(_currentTask.description ?? ""),
|
: Text(_currentTask.description),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(Icons.settings), onPressed: () { },
|
icon: Icon(Icons.settings), onPressed: () { },
|
||||||
),
|
),
|
||||||
|
@ -73,14 +78,14 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
|
||||||
color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black,
|
color: Theme.of(context).brightness == Brightness.dark ? Colors.white : Colors.black,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
) : Text(_currentTask.title ?? ""),
|
) : Text(_currentTask.title),
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
value: _currentTask.done,
|
value: _currentTask.done,
|
||||||
subtitle: widget.showInfo && _currentTask.hasDueDate ?
|
subtitle: widget.showInfo && _currentTask.hasDueDate ?
|
||||||
Text("Due " + durationToHumanReadable(durationUntilDue!), style: TextStyle(color: durationUntilDue.isNegative ? Colors.red : null),)
|
Text("Due " + durationToHumanReadable(durationUntilDue!), style: durationUntilDue.isNegative ? TextStyle(color: Colors.red) : null,)
|
||||||
: _currentTask.description == null || _currentTask.description!.isEmpty
|
: _currentTask.description.isEmpty
|
||||||
? null
|
? null
|
||||||
: Text(_currentTask.description ?? ""),
|
: Text(_currentTask.description),
|
||||||
secondary:
|
secondary:
|
||||||
IconButton(icon: Icon(Icons.settings), onPressed: () {
|
IconButton(icon: Icon(Icons.settings), onPressed: () {
|
||||||
Navigator.push<Task>(
|
Navigator.push<Task>(
|
||||||
|
|
|
@ -1,40 +1,28 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:vikunja_app/models/label.dart';
|
import 'package:vikunja_app/models/label.dart';
|
||||||
import 'package:vikunja_app/theme/constants.dart';
|
|
||||||
|
|
||||||
class LabelComponent extends StatefulWidget {
|
class LabelComponent extends StatelessWidget {
|
||||||
final Label label;
|
final Label label;
|
||||||
final VoidCallback onDelete;
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
const LabelComponent({Key? key, required this.label, required this.onDelete})
|
const LabelComponent({Key? key, required this.label, required this.onDelete})
|
||||||
: super(key: key);
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
|
||||||
State<StatefulWidget> createState() {
|
|
||||||
return new LabelComponentState();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LabelComponentState extends State<LabelComponent> {
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color backgroundColor = widget.label.color;
|
|
||||||
Color textColor =
|
|
||||||
backgroundColor.computeLuminance() > 0.5 ? vLabelDark : vLabelLight;
|
|
||||||
|
|
||||||
return Chip(
|
return Chip(
|
||||||
label: Text(
|
label: Text(
|
||||||
widget.label.title ?? "",
|
label.title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: textColor,
|
color: label.textColor,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: label.color,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(3)),
|
borderRadius: BorderRadius.all(Radius.circular(3)),
|
||||||
),
|
),
|
||||||
onDeleted: widget.onDelete,
|
onDeleted: onDelete,
|
||||||
deleteIconColor: textColor,
|
deleteIconColor: label.textColor,
|
||||||
deleteIcon: Container(
|
deleteIcon: Container(
|
||||||
padding: EdgeInsets.all(3),
|
padding: EdgeInsets.all(3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
@ -43,7 +31,7 @@ class LabelComponentState extends State<LabelComponent> {
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
color: textColor,
|
color: label.textColor,
|
||||||
size: 15,
|
size: 15,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -158,29 +158,33 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
||||||
requestIOSPermissions(notificationsPlugin);
|
requestIOSPermissions(notificationsPlugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
void scheduleDueNotifications() {
|
Future<void> scheduleDueNotifications() async {
|
||||||
notificationsPlugin.cancelAll().then((value) {
|
await notificationsPlugin.cancelAll();
|
||||||
taskService.getAll().then((value) =>
|
final tasks = await taskService.getAll();
|
||||||
value.forEach((task) {
|
for (final task in tasks) {
|
||||||
if(task.reminderDates != null)
|
for (final reminder in task.reminderDates) {
|
||||||
task.reminderDates!.forEach((reminder) {
|
scheduleNotification(
|
||||||
scheduleNotification("Reminder", "This is your reminder for '" + task.title! + "'",
|
"Reminder",
|
||||||
notificationsPlugin,
|
"This is your reminder for '" + task.title + "'",
|
||||||
reminder!,
|
notificationsPlugin,
|
||||||
currentTimeZone,
|
reminder,
|
||||||
platformChannelSpecificsReminders,
|
currentTimeZone,
|
||||||
id: (reminder.millisecondsSinceEpoch/1000).floor());
|
platformChannelSpecificsReminders,
|
||||||
});
|
id: (reminder.millisecondsSinceEpoch / 1000).floor(),
|
||||||
if(task.dueDate != null)
|
);
|
||||||
scheduleNotification("Due Reminder","The task '" + task.title! + "' is due.",
|
}
|
||||||
notificationsPlugin,
|
if (task.hasDueDate) {
|
||||||
task.dueDate!,
|
scheduleNotification(
|
||||||
currentTimeZone,
|
"Due Reminder",
|
||||||
platformChannelSpecificsDueDate,
|
"The task '" + task.title + "' is due.",
|
||||||
id: task.id);
|
notificationsPlugin,
|
||||||
})
|
task.dueDate!,
|
||||||
);
|
currentTimeZone,
|
||||||
});
|
platformChannelSpecificsDueDate,
|
||||||
|
id: task.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -215,7 +219,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
client.configure(token: token, base: base, authenticated: true);
|
client.configure(token: token, base: base, authenticated: true);
|
||||||
var loadedCurrentUser;
|
User loadedCurrentUser;
|
||||||
try {
|
try {
|
||||||
loadedCurrentUser = await UserAPIService(client).getCurrentUser();
|
loadedCurrentUser = await UserAPIService(client).getCurrentUser();
|
||||||
} on ApiException catch (e) {
|
} on ApiException catch (e) {
|
||||||
|
@ -233,9 +237,9 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadedCurrentUser = User(int.tryParse(currentUser)!, "", "");
|
loadedCurrentUser = User(id: int.parse(currentUser), username: '');
|
||||||
} catch (otherExceptions) {
|
} catch (otherExceptions) {
|
||||||
loadedCurrentUser = User(int.tryParse(currentUser)!, "", "");
|
loadedCurrentUser = User(id: int.parse(currentUser), username: '');
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_currentUser = loadedCurrentUser;
|
_currentUser = loadedCurrentUser;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
import 'package:vikunja_app/models/task.dart';
|
import 'package:vikunja_app/models/task.dart';
|
||||||
|
@ -7,55 +6,58 @@ import 'package:vikunja_app/models/user.dart';
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Bucket {
|
class Bucket {
|
||||||
int id, listId, limit;
|
int id, listId, limit;
|
||||||
String? title;
|
String title;
|
||||||
double position;
|
double? position;
|
||||||
DateTime? created, updated;
|
late final DateTime created, updated;
|
||||||
User? createdBy;
|
User createdBy;
|
||||||
bool isDoneBucket;
|
bool isDoneBucket;
|
||||||
|
late final List<Task> tasks;
|
||||||
|
|
||||||
Bucket({
|
Bucket({
|
||||||
required this.id,
|
this.id = -1,
|
||||||
required this.listId,
|
required this.listId,
|
||||||
this.title,
|
required this.title,
|
||||||
this.position = 0,
|
this.position,
|
||||||
required this.limit,
|
required this.limit,
|
||||||
this.isDoneBucket = false,
|
this.isDoneBucket = false,
|
||||||
this.created,
|
DateTime? created,
|
||||||
this.updated,
|
DateTime? updated,
|
||||||
this.createdBy,
|
required this.createdBy,
|
||||||
this.tasks = const <Task>[],
|
List<Task>? tasks,
|
||||||
});
|
}) {
|
||||||
|
this.created = created ?? DateTime.now();
|
||||||
List<Task> tasks = [];
|
this.updated = created ?? DateTime.now();
|
||||||
|
this.tasks = tasks ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
Bucket.fromJSON(Map<String, dynamic> json)
|
Bucket.fromJSON(Map<String, dynamic> json)
|
||||||
: id = json['id'],
|
: id = json['id'],
|
||||||
listId = json['list_id'],
|
listId = json['list_id'],
|
||||||
title = json['title'],
|
title = json['title'],
|
||||||
position = json['position'] is int
|
position = json['position'] is int
|
||||||
? json['position'].toDouble()
|
? json['position'].toDouble()
|
||||||
: json['position'],
|
: json['position'],
|
||||||
limit = json['limit'],
|
limit = json['limit'],
|
||||||
isDoneBucket = json['is_done_bucket'],
|
isDoneBucket = json['is_done_bucket'],
|
||||||
created = DateTime.parse(json['created']),
|
created = DateTime.parse(json['created']),
|
||||||
updated = DateTime.parse(json['updated']),
|
updated = DateTime.parse(json['updated']),
|
||||||
createdBy = json['created_by'] == null
|
createdBy = User.fromJson(json['created_by']),
|
||||||
? null
|
tasks = json['tasks'] == null
|
||||||
: User.fromJson(json['created_by']),
|
? []
|
||||||
tasks = (((json['tasks'] == null) ? [] : json['tasks']) as List<dynamic>)
|
: (json['tasks'] as List<dynamic>)
|
||||||
.map((task) => Task.fromJson(task))
|
.map((task) => Task.fromJson(task))
|
||||||
.cast<Task>()
|
.toList();
|
||||||
.toList();
|
|
||||||
|
|
||||||
toJSON() => {
|
toJSON() => {
|
||||||
'id': id,
|
'id': id != -1 ? id : null,
|
||||||
'list_id': listId,
|
'list_id': listId,
|
||||||
'title': title,
|
'title': title,
|
||||||
'position': position,
|
'position': position,
|
||||||
'limit': limit,
|
'limit': limit,
|
||||||
'is_done_bucket': isDoneBucket,
|
'is_done_bucket': isDoneBucket,
|
||||||
'created': created?.toUtc().toIso8601String(),
|
'created': created.toUtc().toIso8601String(),
|
||||||
'updated': updated?.toUtc().toIso8601String(),
|
'updated': updated.toUtc().toIso8601String(),
|
||||||
'createdBy': createdBy?.toJSON(),
|
'created_by': createdBy.toJSON(),
|
||||||
'tasks': tasks.map((task) => task.toJSON()).toList(),
|
'tasks': tasks.map((task) => task.toJSON()).toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -5,42 +5,45 @@ import 'package:vikunja_app/theme/constants.dart';
|
||||||
|
|
||||||
class Label {
|
class Label {
|
||||||
final int id;
|
final int id;
|
||||||
final String? title, description;
|
final String title, description;
|
||||||
final DateTime? created, updated;
|
late final DateTime created, updated;
|
||||||
final User? createdBy;
|
final User createdBy;
|
||||||
final Color color;
|
final Color? color;
|
||||||
|
|
||||||
Label(
|
late final Color textColor = color != null && color!.computeLuminance() <= 0.5 ? vLabelLight : vLabelDark;
|
||||||
{
|
|
||||||
required this.id,
|
Label({
|
||||||
this.title,
|
this.id = -1,
|
||||||
this.description,
|
required this.title,
|
||||||
this.color = vLabelDefaultColor,
|
this.description = '',
|
||||||
this.created,
|
this.color,
|
||||||
this.updated,
|
DateTime? created,
|
||||||
this.createdBy});
|
DateTime? updated,
|
||||||
|
required this.createdBy,
|
||||||
|
}) {
|
||||||
|
this.created = created ?? DateTime.now();
|
||||||
|
this.updated = updated ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
Label.fromJson(Map<String, dynamic> json)
|
Label.fromJson(Map<String, dynamic> json)
|
||||||
: id = json['id'],
|
: id = json['id'],
|
||||||
title = json['title'],
|
title = json['title'],
|
||||||
description = json['description'],
|
description = json['description'],
|
||||||
color = json['hex_color'] == ''
|
color = json['hex_color'] == ''
|
||||||
? vLabelDefaultColor
|
? null
|
||||||
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
|
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
|
||||||
updated = DateTime.parse(json['updated']),
|
updated = DateTime.parse(json['updated']),
|
||||||
created = DateTime.parse(json['created']),
|
created = DateTime.parse(json['created']),
|
||||||
createdBy = User.fromJson(json['created_by']);
|
createdBy = User.fromJson(json['created_by']);
|
||||||
|
|
||||||
toJSON() => {
|
toJSON() => {
|
||||||
'id': id,
|
'id': id != -1 ? id : null,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'hex_color':
|
'hex_color':
|
||||||
color.value.toRadixString(16).padLeft(8, '0').substring(2),
|
color?.value.toRadixString(16).padLeft(8, '0').substring(2),
|
||||||
'created_by': createdBy?.toJSON(),
|
'created_by': createdBy.toJSON(),
|
||||||
'updated': updated?.toUtc().toIso8601String(),
|
'updated': updated.toUtc().toIso8601String(),
|
||||||
'created': created?.toUtc().toIso8601String(),
|
'created': created.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Color get textColor => color != null && color.computeLuminance() <= 0.5 ? vLabelLight : vLabelDark;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:vikunja_app/models/label.dart';
|
import 'package:vikunja_app/models/label.dart';
|
||||||
import 'package:vikunja_app/models/task.dart';
|
import 'package:vikunja_app/models/task.dart';
|
||||||
|
import 'package:vikunja_app/models/user.dart';
|
||||||
|
|
||||||
class LabelTask {
|
class LabelTask {
|
||||||
final Label label;
|
final Label label;
|
||||||
|
@ -8,8 +9,8 @@ class LabelTask {
|
||||||
|
|
||||||
LabelTask({required this.label, required this.task});
|
LabelTask({required this.label, required this.task});
|
||||||
|
|
||||||
LabelTask.fromJson(Map<String, dynamic> json)
|
LabelTask.fromJson(Map<String, dynamic> json, User createdBy)
|
||||||
: label = new Label(id: json['label_id']),
|
: label = new Label(id: json['label_id'], title: '', createdBy: createdBy),
|
||||||
task = null;
|
task = null;
|
||||||
|
|
||||||
toJSON() => {
|
toJSON() => {
|
||||||
|
|
|
@ -1,50 +1,53 @@
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
import 'package:vikunja_app/models/task.dart';
|
import 'package:vikunja_app/models/task.dart';
|
||||||
import 'package:vikunja_app/models/user.dart';
|
import 'package:vikunja_app/models/user.dart';
|
||||||
|
|
||||||
class TaskList {
|
class TaskList {
|
||||||
final int id;
|
final int id;
|
||||||
int namespaceId;
|
int namespaceId;
|
||||||
String? title, description;
|
String title, description;
|
||||||
final User? owner;
|
final User owner;
|
||||||
final DateTime? created, updated;
|
late final DateTime created, updated;
|
||||||
List<Task?> tasks;
|
late final List<Task> tasks;
|
||||||
final bool isFavorite;
|
final bool isFavorite;
|
||||||
|
|
||||||
TaskList({
|
TaskList({
|
||||||
required this.id,
|
this.id = -1,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.namespaceId,
|
required this.namespaceId,
|
||||||
this.description,
|
this.description = '',
|
||||||
this.owner,
|
required this.owner,
|
||||||
this.created,
|
DateTime? created,
|
||||||
this.updated,
|
DateTime? updated,
|
||||||
this.tasks = const <Task>[],
|
List<Task>? tasks,
|
||||||
this.isFavorite = false,
|
this.isFavorite = false,
|
||||||
});
|
}) {
|
||||||
|
this.created = created ?? DateTime.now();
|
||||||
|
this.updated = updated ?? DateTime.now();
|
||||||
|
this.tasks = tasks ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
TaskList.fromJson(Map<String, dynamic> json)
|
TaskList.fromJson(Map<String, dynamic> json)
|
||||||
: id = json['id'],
|
: id = json['id'],
|
||||||
owner = json['owner'] == null ? null : User.fromJson(json['owner']),
|
owner = User.fromJson(json['owner']),
|
||||||
description = json['description'],
|
description = json['description'],
|
||||||
title = json['title'],
|
title = json['title'],
|
||||||
updated = DateTime.parse(json['updated']),
|
updated = DateTime.parse(json['updated']),
|
||||||
created = DateTime.parse(json['created']),
|
created = DateTime.parse(json['created']),
|
||||||
isFavorite = json['is_favorite'],
|
isFavorite = json['is_favorite'],
|
||||||
namespaceId = json['namespace_id'],
|
namespaceId = json['namespace_id'],
|
||||||
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
|
tasks = json['tasks'] == null ? [] : (json['tasks'] as List<dynamic>)
|
||||||
.map((taskJson) => Task.fromJson(taskJson))
|
.map((taskJson) => Task.fromJson(taskJson))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
toJSON() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
"id": this.id,
|
'id': id != -1 ? id : null,
|
||||||
"title": this.title,
|
'title': title,
|
||||||
"description": this.description,
|
'description': description,
|
||||||
"owner": this.owner?.toJSON(),
|
'owner': owner.toJSON(),
|
||||||
"created": this.created?.toIso8601String(),
|
'created': created.toIso8601String(),
|
||||||
"updated": this.updated?.toIso8601String(),
|
'updated': updated.toIso8601String(),
|
||||||
"namespace_id": this.namespaceId
|
'namespace_id': namespaceId
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import 'package:vikunja_app/models/user.dart';
|
import 'package:vikunja_app/models/user.dart';
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
class Namespace {
|
class Namespace {
|
||||||
final int id;
|
final int id;
|
||||||
final DateTime? created, updated;
|
late final DateTime created, updated;
|
||||||
final String? title, description;
|
final String title, description;
|
||||||
final User? owner;
|
final User owner;
|
||||||
|
|
||||||
Namespace(
|
Namespace({
|
||||||
{required this.id,
|
this.id = -1,
|
||||||
this.created,
|
DateTime? created,
|
||||||
this.updated,
|
DateTime? updated,
|
||||||
required this.title,
|
required this.title,
|
||||||
this.description,
|
this.description = '',
|
||||||
this.owner});
|
required this.owner,
|
||||||
|
}) {
|
||||||
|
this.created = created ?? DateTime.now();
|
||||||
|
this.updated = updated ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
Namespace.fromJson(Map<String, dynamic> json)
|
Namespace.fromJson(Map<String, dynamic> json)
|
||||||
: title = json['title'],
|
: title = json['title'],
|
||||||
|
@ -21,13 +24,14 @@ class Namespace {
|
||||||
id = json['id'],
|
id = json['id'],
|
||||||
created = DateTime.parse(json['created']),
|
created = DateTime.parse(json['created']),
|
||||||
updated = DateTime.parse(json['updated']),
|
updated = DateTime.parse(json['updated']),
|
||||||
owner = json['owner'] == null ? null : User.fromJson(json['owner']);
|
owner = User.fromJson(json['owner']);
|
||||||
|
|
||||||
toJSON() => {
|
toJSON() => {
|
||||||
"created": created?.toIso8601String(),
|
'id': id != -1 ? id : null,
|
||||||
"updated": updated?.toIso8601String(),
|
'created': created.toIso8601String(),
|
||||||
"title": title,
|
'updated': updated.toIso8601String(),
|
||||||
"owner": owner?.toJSON(),
|
'title': title,
|
||||||
"description": description
|
'owner': owner.toJSON(),
|
||||||
|
'description': description
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,57 +1,63 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
import 'package:vikunja_app/components/date_extension.dart';
|
|
||||||
|
|
||||||
import 'package:vikunja_app/models/label.dart';
|
import 'package:vikunja_app/models/label.dart';
|
||||||
import 'package:vikunja_app/models/user.dart';
|
import 'package:vikunja_app/models/user.dart';
|
||||||
import 'package:vikunja_app/models/taskAttachment.dart';
|
import 'package:vikunja_app/models/taskAttachment.dart';
|
||||||
import 'package:vikunja_app/theme/constants.dart';
|
|
||||||
import 'package:vikunja_app/utils/checkboxes_in_text.dart';
|
import 'package:vikunja_app/utils/checkboxes_in_text.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class Task {
|
class Task {
|
||||||
final int? id, parentTaskId, priority, listId, bucketId;
|
final int id;
|
||||||
final DateTime? created, updated, dueDate, startDate, endDate;
|
final int? parentTaskId, priority, bucketId;
|
||||||
final List<DateTime?>? reminderDates;
|
final int listId;
|
||||||
final String identifier;
|
late final DateTime created, updated;
|
||||||
final String? title, description;
|
final DateTime? dueDate, startDate, endDate;
|
||||||
|
final List<DateTime> reminderDates;
|
||||||
|
final String? identifier;
|
||||||
|
final String title, description;
|
||||||
final bool done;
|
final bool done;
|
||||||
final Color color;
|
final Color? color;
|
||||||
final double? kanbanPosition;
|
final double? kanbanPosition;
|
||||||
final User? createdBy;
|
final User createdBy;
|
||||||
final Duration? repeatAfter;
|
final Duration? repeatAfter;
|
||||||
final List<Task>? subtasks;
|
final List<Task> subtasks;
|
||||||
final List<Label>? labels;
|
final List<Label> labels;
|
||||||
final List<TaskAttachment>? attachments;
|
final List<TaskAttachment> attachments;
|
||||||
// TODO: add position(?)
|
// TODO: add position(?)
|
||||||
|
|
||||||
CheckboxStatistics? _checkboxStatistics;
|
late final CheckboxStatistics checkboxStatistics = getCheckboxStatistics(description);
|
||||||
|
late final hasCheckboxes = checkboxStatistics.total != 0;
|
||||||
|
late final textColor = (color != null && color!.computeLuminance() > 0.5) ? Colors.black : Colors.white;
|
||||||
|
late final hasDueDate = dueDate?.year != 1;
|
||||||
|
|
||||||
// // TODO: use `late final` once upgraded to current dart version
|
|
||||||
Task({
|
Task({
|
||||||
required this.id,
|
this.id = -1,
|
||||||
required this.identifier,
|
this.identifier,
|
||||||
this.title,
|
this.title = '',
|
||||||
this.description,
|
this.description = '',
|
||||||
this.done = false,
|
this.done = false,
|
||||||
this.reminderDates,
|
this.reminderDates = const [],
|
||||||
this.dueDate,
|
this.dueDate,
|
||||||
this.startDate,
|
this.startDate,
|
||||||
this.endDate,
|
this.endDate,
|
||||||
this.parentTaskId,
|
this.parentTaskId,
|
||||||
this.priority,
|
this.priority,
|
||||||
this.repeatAfter,
|
this.repeatAfter,
|
||||||
this.color = vBlue, // TODO: decide on color
|
this.color,
|
||||||
this.kanbanPosition,
|
this.kanbanPosition,
|
||||||
this.subtasks,
|
this.subtasks = const [],
|
||||||
this.labels,
|
this.labels = const [],
|
||||||
this.attachments,
|
this.attachments = const [],
|
||||||
this.created,
|
DateTime? created,
|
||||||
this.updated,
|
DateTime? updated,
|
||||||
this.createdBy,
|
required this.createdBy,
|
||||||
this.listId,
|
required this.listId,
|
||||||
this.bucketId,
|
this.bucketId,
|
||||||
});
|
}) {
|
||||||
|
this.created = DateTime.now();
|
||||||
|
this.updated = DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
bool loading = false;
|
bool loading = false;
|
||||||
|
|
||||||
|
@ -61,93 +67,85 @@ class Task {
|
||||||
description = json['description'],
|
description = json['description'],
|
||||||
identifier = json['identifier'],
|
identifier = json['identifier'],
|
||||||
done = json['done'],
|
done = json['done'],
|
||||||
reminderDates = json['reminder_dates'] != null ? (json['reminder_dates'] as List<dynamic>)
|
reminderDates = json['reminder_dates'] != null
|
||||||
.map((ts) => DateTime.parse(ts))
|
? (json['reminder_dates'] as List<dynamic>)
|
||||||
.cast<DateTime>()
|
.map((ts) => DateTime.parse(ts))
|
||||||
.toList() : null,
|
.toList()
|
||||||
|
: [],
|
||||||
dueDate = DateTime.parse(json['due_date']),
|
dueDate = DateTime.parse(json['due_date']),
|
||||||
startDate = DateTime.parse(json['start_date']),
|
startDate = DateTime.parse(json['start_date']),
|
||||||
endDate = DateTime.parse(json['end_date']),
|
endDate = DateTime.parse(json['end_date']),
|
||||||
parentTaskId = json['parent_task_id'],
|
parentTaskId = json['parent_task_id'],
|
||||||
priority = json['priority'],
|
priority = json['priority'],
|
||||||
repeatAfter = Duration(seconds: json['repeat_after']),
|
repeatAfter = Duration(seconds: json['repeat_after']),
|
||||||
color = json['hex_color'] == ''
|
color = json['hex_color'] != ''
|
||||||
? vBlue
|
? Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000)
|
||||||
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
|
: null,
|
||||||
kanbanPosition = json['kanban_position'] is int
|
kanbanPosition = json['kanban_position'] is int
|
||||||
? json['kanban_position'].toDouble()
|
? json['kanban_position'].toDouble()
|
||||||
: json['kanban_position'],
|
: json['kanban_position'],
|
||||||
labels = ((json['labels'] ?? []) as List<dynamic>)
|
labels = json['labels'] != null
|
||||||
.map((label) => Label.fromJson(label))
|
? (json['labels'] as List<dynamic>)
|
||||||
.cast<Label>()
|
.map((label) => Label.fromJson(label))
|
||||||
.toList(),
|
.toList()
|
||||||
subtasks = ((json['subtasks'] ?? []) as List<dynamic>)
|
: [],
|
||||||
.map((subtask) => Task.fromJson(subtask))
|
subtasks = json['subtasks'] != null
|
||||||
.cast<Task>()
|
? (json['subtasks'] as List<dynamic>)
|
||||||
.toList(),
|
.map((subtask) => Task.fromJson(subtask))
|
||||||
attachments = ((json['attachments'] ?? []) as List<dynamic>)
|
.toList()
|
||||||
.map((attachment) => TaskAttachment.fromJSON(attachment))
|
: [],
|
||||||
.cast<TaskAttachment>()
|
attachments = json['attachments'] != null
|
||||||
.toList(),
|
? (json['attachments'] as List<dynamic>)
|
||||||
|
.map((attachment) => TaskAttachment.fromJSON(attachment))
|
||||||
|
.toList()
|
||||||
|
: [],
|
||||||
updated = DateTime.parse(json['updated']),
|
updated = DateTime.parse(json['updated']),
|
||||||
created = DateTime.parse(json['created']),
|
created = DateTime.parse(json['created']),
|
||||||
listId = json['list_id'],
|
listId = json['list_id'],
|
||||||
bucketId = json['bucket_id'],
|
bucketId = json['bucket_id'],
|
||||||
createdBy = json['created_by'] == null
|
createdBy = User.fromJson(json['created_by']);
|
||||||
? null
|
|
||||||
: User.fromJson(json['created_by']);
|
|
||||||
|
|
||||||
toJSON() => {
|
toJSON() => {
|
||||||
'id': id,
|
'id': id != -1 ? id : null,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': description,
|
'description': description,
|
||||||
'identifier': identifier,
|
'identifier': identifier,
|
||||||
'done': done,
|
'done': done,
|
||||||
'reminder_dates':
|
'reminder_dates': reminderDates
|
||||||
reminderDates?.map((date) => date?.toUtc().toIso8601String()).toList(),
|
.map((date) => date.toUtc().toIso8601String())
|
||||||
|
.toList(),
|
||||||
'due_date': dueDate?.toUtc().toIso8601String(),
|
'due_date': dueDate?.toUtc().toIso8601String(),
|
||||||
'start_date': startDate?.toUtc().toIso8601String(),
|
'start_date': startDate?.toUtc().toIso8601String(),
|
||||||
'end_date': endDate?.toUtc().toIso8601String(),
|
'end_date': endDate?.toUtc().toIso8601String(),
|
||||||
'priority': priority,
|
'priority': priority,
|
||||||
'repeat_after': repeatAfter?.inSeconds,
|
'repeat_after': repeatAfter?.inSeconds,
|
||||||
'hex_color': color.value.toRadixString(16).padLeft(8, '0').substring(2),
|
'hex_color': color?.value.toRadixString(16).padLeft(8, '0').substring(2),
|
||||||
'kanban_position': kanbanPosition,
|
'kanban_position': kanbanPosition,
|
||||||
'labels': labels?.map((label) => label.toJSON()).toList(),
|
'labels': labels.map((label) => label.toJSON()).toList(),
|
||||||
'subtasks': subtasks?.map((subtask) => subtask.toJSON()).toList(),
|
'subtasks': subtasks.map((subtask) => subtask.toJSON()).toList(),
|
||||||
'attachments': attachments?.map((attachment) => attachment.toJSON()).toList(),
|
'attachments':
|
||||||
|
attachments.map((attachment) => attachment.toJSON()).toList(),
|
||||||
'bucket_id': bucketId,
|
'bucket_id': bucketId,
|
||||||
'created_by': createdBy?.toJSON(),
|
'created_by': createdBy.toJSON(),
|
||||||
'updated': updated?.toUtc().toIso8601String(),
|
'updated': updated.toUtc().toIso8601String(),
|
||||||
'created': created?.toUtc().toIso8601String(),
|
'created': created.toUtc().toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
Color? get textColor => color.computeLuminance() > 0.5 ? Colors.black : Colors.white;
|
|
||||||
|
|
||||||
CheckboxStatistics? get checkboxStatistics {
|
|
||||||
if (_checkboxStatistics != null)
|
|
||||||
return _checkboxStatistics;
|
|
||||||
if (description!.isEmpty)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
_checkboxStatistics = getCheckboxStatistics(description!);
|
|
||||||
return _checkboxStatistics;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasCheckboxes {
|
|
||||||
final checkboxStatistics = this.checkboxStatistics;
|
|
||||||
if (checkboxStatistics != null && checkboxStatistics.total != 0)
|
|
||||||
return true;
|
|
||||||
else
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool get hasDueDate => dueDate?.year != 1;
|
|
||||||
|
|
||||||
Task copyWith({
|
Task copyWith({
|
||||||
int? id, int? parentTaskId, int? priority, int? listId, int? bucketId,
|
int? id,
|
||||||
DateTime? created, DateTime? updated, DateTime? dueDate, DateTime? startDate, DateTime? endDate,
|
int? parentTaskId,
|
||||||
List<DateTime?>? reminderDates,
|
int? priority,
|
||||||
String? title, String? description, String? identifier,
|
int? listId,
|
||||||
|
int? bucketId,
|
||||||
|
DateTime? created,
|
||||||
|
DateTime? updated,
|
||||||
|
DateTime? dueDate,
|
||||||
|
DateTime? startDate,
|
||||||
|
DateTime? endDate,
|
||||||
|
List<DateTime>? reminderDates,
|
||||||
|
String? title,
|
||||||
|
String? description,
|
||||||
|
String? identifier,
|
||||||
bool? done,
|
bool? done,
|
||||||
Color? color,
|
Color? color,
|
||||||
bool? resetColor,
|
bool? resetColor,
|
||||||
|
@ -160,27 +158,27 @@ class Task {
|
||||||
}) {
|
}) {
|
||||||
return Task(
|
return Task(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
parentTaskId: parentTaskId,
|
parentTaskId: parentTaskId ?? this.parentTaskId,
|
||||||
priority: priority,
|
priority: priority ?? this.priority,
|
||||||
listId: listId,
|
listId: listId ?? this.listId,
|
||||||
bucketId: bucketId,
|
bucketId: bucketId ?? this.bucketId,
|
||||||
created: created,
|
created: created ?? this.created,
|
||||||
updated: updated,
|
updated: updated ?? this.updated,
|
||||||
dueDate: dueDate,
|
dueDate: dueDate ?? this.dueDate,
|
||||||
startDate: startDate,
|
startDate: startDate ?? this.startDate,
|
||||||
endDate: endDate,
|
endDate: endDate ?? this.endDate,
|
||||||
reminderDates: reminderDates,
|
reminderDates: reminderDates ?? this.reminderDates,
|
||||||
title: title,
|
title: title ?? this.title,
|
||||||
description: description,
|
description: description ?? this.description,
|
||||||
identifier: identifier ?? this.identifier,
|
identifier: identifier ?? this.identifier,
|
||||||
done: done ?? this.done,
|
done: done ?? this.done,
|
||||||
color: (resetColor ?? false) ? vBlue : (color ?? this.color),
|
color: (resetColor ?? false) ? null : (color ?? this.color),
|
||||||
kanbanPosition: kanbanPosition,
|
kanbanPosition: kanbanPosition ?? this.kanbanPosition,
|
||||||
createdBy: createdBy,
|
createdBy: createdBy ?? this.createdBy,
|
||||||
repeatAfter: repeatAfter,
|
repeatAfter: repeatAfter ?? this.repeatAfter,
|
||||||
subtasks: subtasks,
|
subtasks: subtasks ?? this.subtasks,
|
||||||
labels: labels,
|
labels: labels ?? this.labels,
|
||||||
attachments: attachments,
|
attachments: attachments ?? this.attachments,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,33 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
import 'package:vikunja_app/models/user.dart';
|
import 'package:vikunja_app/models/user.dart';
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class TaskAttachment {
|
class TaskAttachment {
|
||||||
int id, taskId;
|
final int id, taskId;
|
||||||
DateTime? created;
|
late final DateTime created;
|
||||||
User? createdBy;
|
final User createdBy;
|
||||||
// TODO: add file
|
// TODO: add file
|
||||||
|
|
||||||
TaskAttachment({
|
TaskAttachment({
|
||||||
required this.id,
|
this.id = -1,
|
||||||
required this.taskId,
|
required this.taskId,
|
||||||
this.created,
|
DateTime? created,
|
||||||
this.createdBy,
|
required this.createdBy,
|
||||||
});
|
}) {
|
||||||
|
this.created = created ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
TaskAttachment.fromJSON(Map<String, dynamic> json)
|
TaskAttachment.fromJSON(Map<String, dynamic> json)
|
||||||
: id = json['id'],
|
: id = json['id'],
|
||||||
taskId = json['task_id'],
|
taskId = json['task_id'],
|
||||||
created = DateTime.parse(json['created']),
|
created = DateTime.parse(json['created']),
|
||||||
createdBy = json['created_by'] == null
|
createdBy = User.fromJson(json['created_by']);
|
||||||
? null
|
|
||||||
: User.fromJson(json['created_by']);
|
|
||||||
|
|
||||||
toJSON() => {
|
toJSON() => {
|
||||||
'id': id,
|
'id': id != -1 ? id : null,
|
||||||
'task_id': taskId,
|
'task_id': taskId,
|
||||||
'created': created?.toUtc().toIso8601String(),
|
'created': created.toUtc().toIso8601String(),
|
||||||
'created_by': createdBy?.toJSON(),
|
'created_by': createdBy.toJSON(),
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -3,18 +3,37 @@ import 'package:vikunja_app/global.dart';
|
||||||
|
|
||||||
class User {
|
class User {
|
||||||
final int id;
|
final int id;
|
||||||
final String email, username;
|
final String name, username;
|
||||||
|
late final DateTime created, updated;
|
||||||
|
|
||||||
|
User({
|
||||||
|
this.id = -1,
|
||||||
|
this.name = '',
|
||||||
|
required this.username,
|
||||||
|
DateTime? created,
|
||||||
|
DateTime? updated,
|
||||||
|
}) {
|
||||||
|
this.created = created ?? DateTime.now();
|
||||||
|
this.updated = updated ?? DateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
User(this.id, this.email, this.username);
|
|
||||||
User.fromJson(Map<String, dynamic> json)
|
User.fromJson(Map<String, dynamic> json)
|
||||||
: id = json['id'],
|
: id = json['id'],
|
||||||
email = json.containsKey('email') ? json['email'] : '',
|
name = json.containsKey('name') ? json['name'] : '',
|
||||||
username = json['username'];
|
username = json['username'],
|
||||||
|
created = DateTime.parse(json['created']),
|
||||||
|
updated = DateTime.parse(json['updated']);
|
||||||
|
|
||||||
toJSON() => {"id": this.id, "email": this.email, "username": this.username};
|
toJSON() => {
|
||||||
|
'id': id != -1 ? id : null,
|
||||||
|
'name': name,
|
||||||
|
'username': username,
|
||||||
|
'created': created.toUtc().toIso8601String(),
|
||||||
|
'updated': updated.toUtc().toIso8601String(),
|
||||||
|
};
|
||||||
|
|
||||||
String? avatarUrl(BuildContext context) {
|
String avatarUrl(BuildContext context) {
|
||||||
return VikunjaGlobal.of(context).client.base! + "/avatar/${this.username}";
|
return VikunjaGlobal.of(context).client.base + "/avatar/${this.username}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
|
||||||
.asMap()
|
.asMap()
|
||||||
.forEach((i, namespace) => namespacesList.add(new ListTile(
|
.forEach((i, namespace) => namespacesList.add(new ListTile(
|
||||||
leading: const Icon(Icons.folder),
|
leading: const Icon(Icons.folder),
|
||||||
title: new Text(namespace.title ?? ""),
|
title: new Text(namespace.title),
|
||||||
selected: i == _selectedDrawerIndex,
|
selected: i == _selectedDrawerIndex,
|
||||||
onTap: () => _onSelectItem(i),
|
onTap: () => _onSelectItem(i),
|
||||||
)));
|
)));
|
||||||
|
@ -87,7 +87,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var currentUser = VikunjaGlobal.of(context).currentUser;
|
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||||
if (_selectedDrawerIndex != _previousDrawerIndex || drawerItem == null)
|
if (_selectedDrawerIndex != _previousDrawerIndex || drawerItem == null)
|
||||||
drawerItem = _getDrawerItemWidget(_selectedDrawerIndex);
|
drawerItem = _getDrawerItemWidget(_selectedDrawerIndex);
|
||||||
|
|
||||||
|
@ -107,15 +107,15 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
|
||||||
))).whenComplete(() => _loadNamespaces()))
|
))).whenComplete(() => _loadNamespaces()))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
drawer: new Drawer(
|
drawer: Drawer(
|
||||||
child: new Column(children: <Widget>[
|
child: Column(children: <Widget>[
|
||||||
new UserAccountsDrawerHeader(
|
UserAccountsDrawerHeader(
|
||||||
accountEmail: currentUser?.email == null
|
accountName: currentUser != null
|
||||||
? null
|
? Text(currentUser.username)
|
||||||
: Text(currentUser?.email ?? ""),
|
: null,
|
||||||
accountName: currentUser?.username == null
|
accountEmail: currentUser != null
|
||||||
? null
|
? Text(currentUser.name)
|
||||||
: Text(currentUser?.username ?? ""),
|
: null,
|
||||||
onDetailsPressed: () {
|
onDetailsPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showUserDetails = !_showUserDetails;
|
_showUserDetails = !_showUserDetails;
|
||||||
|
@ -134,12 +134,12 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
|
||||||
Theme.of(context).primaryColor, BlendMode.multiply)),
|
Theme.of(context).primaryColor, BlendMode.multiply)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
new Builder(
|
Builder(
|
||||||
builder: (BuildContext context) => Expanded(
|
builder: (BuildContext context) => Expanded(
|
||||||
child: _showUserDetails
|
child: _showUserDetails
|
||||||
? _userDetailsWidget(context)
|
? _userDetailsWidget(context)
|
||||||
: _namespacesWidget())),
|
: _namespacesWidget())),
|
||||||
new Align(
|
Align(
|
||||||
alignment: FractionalOffset.bottomLeft,
|
alignment: FractionalOffset.bottomLeft,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) => ListTile(
|
builder: (context) => ListTile(
|
||||||
|
@ -151,7 +151,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
new Align(
|
Align(
|
||||||
alignment: FractionalOffset.bottomCenter,
|
alignment: FractionalOffset.bottomCenter,
|
||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (context) => ListTile(
|
builder: (context) => ListTile(
|
||||||
|
@ -197,9 +197,14 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_addNamespace(String name, BuildContext context) {
|
_addNamespace(String name, BuildContext context) {
|
||||||
|
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
VikunjaGlobal.of(context)
|
VikunjaGlobal.of(context)
|
||||||
.namespaceService
|
.namespaceService
|
||||||
.create(Namespace(id: 0, title: name))
|
.create(Namespace(title: name, owner: currentUser))
|
||||||
.then((_) {
|
.then((_) {
|
||||||
_loadNamespaces();
|
_loadNamespaces();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
|
|
@ -103,20 +103,31 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) =>
|
builder: (_) =>
|
||||||
AddDialog(
|
AddDialog(
|
||||||
onAddTask: (task) => _addTask(task, context),
|
onAddTask: (title, dueDate) => _addTask(title, dueDate, context),
|
||||||
decoration: new InputDecoration(
|
decoration: new InputDecoration(
|
||||||
labelText: 'Task Name', hintText: 'eg. Milk')));
|
labelText: 'Task Name', hintText: 'eg. Milk')));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_addTask(Task task, BuildContext context) {
|
Future<void> _addTask(
|
||||||
var globalState = VikunjaGlobal.of(context);
|
String title, DateTime? dueDate, BuildContext context) async {
|
||||||
globalState.taskService.add(defaultList!, task).then((_) {
|
final globalState = VikunjaGlobal.of(context);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
if (globalState.currentUser == null) {
|
||||||
content: Text('The task was added successfully!'),
|
return;
|
||||||
));
|
}
|
||||||
_loadList(context).then((value) => setState((){}));
|
|
||||||
});
|
await globalState.taskService.add(
|
||||||
|
defaultList!,
|
||||||
|
Task(
|
||||||
|
createdBy: globalState.currentUser!,
|
||||||
|
listId: defaultList!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: Text('The task was added successfully!'),
|
||||||
|
));
|
||||||
|
_loadList(context).then((value) => setState(() {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ class _ListPageState extends State<ListPage> {
|
||||||
List<Task> _loadingTasks = [];
|
List<Task> _loadingTasks = [];
|
||||||
int _currentPage = 1;
|
int _currentPage = 1;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
bool? displayDoneTasks;
|
bool displayDoneTasks = false;
|
||||||
ListProvider? taskState;
|
ListProvider? taskState;
|
||||||
PageController? _pageController;
|
PageController? _pageController;
|
||||||
Map<int, BucketProps> _bucketProps = {};
|
Map<int, BucketProps> _bucketProps = {};
|
||||||
|
@ -159,7 +159,7 @@ class _ListPageState extends State<ListPage> {
|
||||||
ListView _listView(BuildContext context) {
|
ListView _listView(BuildContext context) {
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||||
itemCount: taskState!.tasks.length,
|
itemCount: taskState!.tasks.length * 2,
|
||||||
itemBuilder: (context, i) {
|
itemBuilder: (context, i) {
|
||||||
if (i.isOdd) return Divider();
|
if (i.isOdd) return Divider();
|
||||||
|
|
||||||
|
@ -170,11 +170,6 @@ class _ListPageState extends State<ListPage> {
|
||||||
|
|
||||||
final index = i ~/ 2;
|
final index = i ~/ 2;
|
||||||
|
|
||||||
|
|
||||||
// This handles the case if there are no more elements in the list left which can be provided by the api
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
// should never happen due to itemCount
|
|
||||||
if (taskState!.maxPages == _currentPage &&
|
if (taskState!.maxPages == _currentPage &&
|
||||||
index == taskState!.tasks.length)
|
index == taskState!.tasks.length)
|
||||||
throw Exception("Check itemCount attribute");
|
throw Exception("Check itemCount attribute");
|
||||||
|
@ -341,7 +336,7 @@ class _ListPageState extends State<ListPage> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (_bucketProps[bucket.id]!.titleController.text.isEmpty)
|
if (_bucketProps[bucket.id]!.titleController.text.isEmpty)
|
||||||
_bucketProps[bucket.id]!.titleController.text = bucket.title ?? "";
|
_bucketProps[bucket.id]!.titleController.text = bucket.title;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: <Widget>[
|
children: <Widget>[
|
||||||
|
@ -389,7 +384,7 @@ class _ListPageState extends State<ListPage> {
|
||||||
padding: const EdgeInsets.only(right: 2),
|
padding: const EdgeInsets.only(right: 2),
|
||||||
child: Text(
|
child: Text(
|
||||||
'${bucket.tasks.length}/${bucket.limit}',
|
'${bucket.tasks.length}/${bucket.limit}',
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16)).copyWith(
|
||||||
color: bucket.limit != 0 && bucket.tasks.length >= bucket.limit
|
color: bucket.limit != 0 && bucket.tasks.length >= bucket.limit
|
||||||
? Colors.red : null,
|
? Colors.red : null,
|
||||||
),
|
),
|
||||||
|
@ -426,7 +421,7 @@ class _ListPageState extends State<ListPage> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
final bool enableDelete = (taskState?.buckets.length ?? 0) > 1;
|
final bool enableDelete = taskState!.buckets.length > 1;
|
||||||
return <PopupMenuEntry<BucketMenu>>[
|
return <PopupMenuEntry<BucketMenu>>[
|
||||||
PopupMenuItem<BucketMenu>(
|
PopupMenuItem<BucketMenu>(
|
||||||
value: BucketMenu.limit,
|
value: BucketMenu.limit,
|
||||||
|
@ -613,7 +608,7 @@ class _ListPageState extends State<ListPage> {
|
||||||
context: context,
|
context: context,
|
||||||
listId: _list!.id,
|
listId: _list!.id,
|
||||||
page: page,
|
page: page,
|
||||||
displayDoneTasks: displayDoneTasks ?? false
|
displayDoneTasks: displayDoneTasks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -638,15 +633,18 @@ class _ListPageState extends State<ListPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addItem(String title, BuildContext context, [Bucket? bucket]) {
|
Future<void> _addItem(String title, BuildContext context, [Bucket? bucket]) async {
|
||||||
var globalState = VikunjaGlobal.of(context);
|
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||||
var newTask = Task(
|
if (currentUser == null) {
|
||||||
id: null,
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newTask = Task(
|
||||||
title: title,
|
title: title,
|
||||||
createdBy: globalState.currentUser,
|
createdBy: currentUser,
|
||||||
done: false,
|
done: false,
|
||||||
bucketId: bucket?.id,
|
bucketId: bucket?.id,
|
||||||
identifier: '',
|
listId: _list!.id,
|
||||||
);
|
);
|
||||||
setState(() => _loadingTasks.add(newTask));
|
setState(() => _loadingTasks.add(newTask));
|
||||||
return Provider.of<ListProvider>(context, listen: false)
|
return Provider.of<ListProvider>(context, listen: false)
|
||||||
|
@ -679,23 +677,27 @@ class _ListPageState extends State<ListPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _addBucket(String title, BuildContext context) {
|
Future<void> _addBucket(String title, BuildContext context) async {
|
||||||
return Provider.of<ListProvider>(context, listen: false).addBucket(
|
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Provider.of<ListProvider>(context, listen: false).addBucket(
|
||||||
context: context,
|
context: context,
|
||||||
newBucket: Bucket(
|
newBucket: Bucket(
|
||||||
id: 0,
|
|
||||||
title: title,
|
title: title,
|
||||||
createdBy: VikunjaGlobal.of(context).currentUser,
|
createdBy: currentUser,
|
||||||
listId: _list!.id,
|
listId: _list!.id,
|
||||||
limit: 0,
|
limit: 0,
|
||||||
),
|
),
|
||||||
listId: _list!.id,
|
listId: _list!.id,
|
||||||
).then((_) {
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Text('The bucket was added successfully!'),
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
));
|
content: Text('The bucket was added successfully!'),
|
||||||
setState(() {});
|
));
|
||||||
});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateBucket(BuildContext context, Bucket bucket) {
|
Future<void> _updateBucket(BuildContext context, Bucket bucket) {
|
||||||
|
@ -715,16 +717,17 @@ class _ListPageState extends State<ListPage> {
|
||||||
context: context,
|
context: context,
|
||||||
listId: bucket.listId,
|
listId: bucket.listId,
|
||||||
bucketId: bucket.id,
|
bucketId: bucket.id,
|
||||||
).then((_) {
|
);
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
||||||
content: Row(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
children: <Widget>[
|
content: Row(
|
||||||
Text('${bucket.title} was deleted.'),
|
children: <Widget>[
|
||||||
Icon(Icons.delete),
|
Text('${bucket.title} was deleted.'),
|
||||||
],
|
Icon(Icons.delete),
|
||||||
),
|
],
|
||||||
));
|
),
|
||||||
});
|
));
|
||||||
|
|
||||||
_onViewTapped(1);
|
_onViewTapped(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,9 @@ class ListEditPage extends StatefulWidget {
|
||||||
class _ListEditPageState extends State<ListEditPage> {
|
class _ListEditPageState extends State<ListEditPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
String? _title, _description;
|
String _title = '', _description = '';
|
||||||
bool? displayDoneTasks;
|
bool? displayDoneTasks;
|
||||||
int listId = -1;
|
late int listId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState(){
|
void initState(){
|
||||||
|
@ -52,7 +52,7 @@ class _ListEditPageState extends State<ListEditPage> {
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
initialValue: widget.list.title,
|
initialValue: widget.list.title,
|
||||||
onSaved: (title) => _title = title,
|
onSaved: (title) => _title = title ?? '',
|
||||||
validator: (title) {
|
validator: (title) {
|
||||||
//if (title?.length < 3 || title.length > 250) {
|
//if (title?.length < 3 || title.length > 250) {
|
||||||
// return 'The title needs to have between 3 and 250 characters.';
|
// return 'The title needs to have between 3 and 250 characters.';
|
||||||
|
@ -71,7 +71,7 @@ class _ListEditPageState extends State<ListEditPage> {
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
initialValue: widget.list.description,
|
initialValue: widget.list.description,
|
||||||
onSaved: (description) => _description = description,
|
onSaved: (description) => _description = description ?? '',
|
||||||
validator: (description) {
|
validator: (description) {
|
||||||
if(description == null)
|
if(description == null)
|
||||||
return null;
|
return null;
|
||||||
|
@ -88,29 +88,16 @@ class _ListEditPageState extends State<ListEditPage> {
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||||
child: displayDoneTasks != null ?
|
child: CheckboxListTile(
|
||||||
CheckboxListTile(
|
value: displayDoneTasks ?? false,
|
||||||
value: displayDoneTasks,
|
title: Text("Show done tasks"),
|
||||||
title: Text("Show done tasks"),
|
onChanged: (value) {
|
||||||
onChanged: (value) {
|
value ??= false;
|
||||||
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value == false ? "0" : "1");
|
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value ? "1" : "0");
|
||||||
setState(() => displayDoneTasks = value);
|
setState(() => displayDoneTasks = value);
|
||||||
},
|
},
|
||||||
)
|
),
|
||||||
: ListTile(
|
),
|
||||||
trailing:
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: SizedBox(
|
|
||||||
height: Checkbox.width,
|
|
||||||
width: Checkbox.width,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text("Show done task"),
|
|
||||||
),),
|
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) => Padding(
|
builder: (context) => Padding(
|
||||||
padding: EdgeInsets.symmetric(vertical: 10.0),
|
padding: EdgeInsets.symmetric(vertical: 10.0),
|
||||||
|
|
|
@ -32,41 +32,40 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
|
|
||||||
int? _priority;
|
int? _priority;
|
||||||
DateTime? _dueDate, _startDate, _endDate;
|
DateTime? _dueDate, _startDate, _endDate;
|
||||||
List<DateTime?>? _reminderDates;
|
late final List<DateTime> _reminderDates;
|
||||||
String? _title, _description, _repeatAfterType;
|
String? _title, _description, _repeatAfterType;
|
||||||
Duration? _repeatAfter;
|
Duration? _repeatAfter;
|
||||||
List<Label>? _labels;
|
late final List<Label> _labels;
|
||||||
// we use this to find the label object after a user taps on the suggestion, because the typeahead only uses strings, not full objects.
|
// we use this to find the label object after a user taps on the suggestion, because the typeahead only uses strings, not full objects.
|
||||||
List<Label>? _suggestedLabels;
|
List<Label>? _suggestedLabels;
|
||||||
var _reminderInputs = <Widget>[];
|
final _reminderInputs = <Widget>[];
|
||||||
final _labelTypeAheadController = TextEditingController();
|
final _labelTypeAheadController = TextEditingController();
|
||||||
Color? _color;
|
Color? _color;
|
||||||
Color? _pickerColor;
|
Color? _pickerColor;
|
||||||
bool _resetColor = false;
|
bool _resetColor = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_reminderDates = widget.task.reminderDates;
|
||||||
|
|
||||||
|
for (var i = 0; i < _reminderDates.length; i++) {
|
||||||
|
_reminderInputs.add(VikunjaDateTimePicker(
|
||||||
|
initialValue: _reminderDates[i],
|
||||||
|
label: 'Reminder',
|
||||||
|
onSaved: (reminder) {
|
||||||
|
_reminderDates[i] = reminder ?? DateTime(0);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
_labels = widget.task.labels;
|
||||||
|
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) {
|
Widget build(BuildContext ctx) {
|
||||||
// This builds the initial list of reminder inputs only once.
|
|
||||||
if (_reminderDates == null) {
|
|
||||||
_reminderDates = [];
|
|
||||||
widget.task.reminderDates?.forEach((element) { _reminderDates?.add(element ?? null);});
|
|
||||||
|
|
||||||
_reminderDates!.asMap().forEach((i, time) =>
|
|
||||||
setState(() => _reminderInputs.add(VikunjaDateTimePicker(
|
|
||||||
initialValue: time,
|
|
||||||
label: 'Reminder',
|
|
||||||
onSaved: (reminder) {
|
|
||||||
_reminderDates![i] = reminder;
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
)))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_labels == null) {
|
|
||||||
_labels = widget.task.labels ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () {
|
onWillPop: () {
|
||||||
if(_changed) {
|
if(_changed) {
|
||||||
|
@ -221,15 +220,15 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
),
|
),
|
||||||
onTap: () {
|
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.
|
// 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);
|
_reminderDates.add(DateTime(0));
|
||||||
var currentIndex = _reminderDates!.length - 1;
|
var currentIndex = _reminderDates.length - 1;
|
||||||
|
|
||||||
// FIXME: Why does putting this into a row fails?
|
// FIXME: Why does putting this into a row fails?
|
||||||
setState(() => _reminderInputs.add(
|
setState(() => _reminderInputs.add(
|
||||||
VikunjaDateTimePicker(
|
VikunjaDateTimePicker(
|
||||||
label: 'Reminder',
|
label: 'Reminder',
|
||||||
onSaved: (reminder) =>
|
onSaved: (reminder) =>
|
||||||
_reminderDates![currentIndex] = reminder,
|
_reminderDates[currentIndex] = reminder ?? DateTime(0),
|
||||||
onChanged: (_) => _changed = true,
|
onChanged: (_) => _changed = true,
|
||||||
initialValue: DateTime.now(),
|
initialValue: DateTime.now(),
|
||||||
),
|
),
|
||||||
|
@ -262,7 +261,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
),
|
),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 10,
|
spacing: 10,
|
||||||
children: _labels!.map((Label label) {
|
children: _labels.map((Label label) {
|
||||||
return LabelComponent(
|
return LabelComponent(
|
||||||
label: label,
|
label: label,
|
||||||
onDelete: () {
|
onDelete: () {
|
||||||
|
@ -312,9 +311,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Color',
|
'Color',
|
||||||
style: _resetColor ? null : TextStyle(
|
style: (_resetColor || (_color ?? widget.task.color) == null) ? null : TextStyle(
|
||||||
color: (_color ?? widget.task.color)
|
color: (_color ?? widget.task.color)!.computeLuminance() > 0.5 ? Colors.black : Colors.white,
|
||||||
.computeLuminance() > 0.5 ? Colors.black : Colors.white,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: _resetColor ? null : ButtonStyle(
|
style: _resetColor ? null : ButtonStyle(
|
||||||
|
@ -364,7 +362,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
setState(() => _loading = true);
|
setState(() => _loading = true);
|
||||||
|
|
||||||
// Removes all reminders with no value set.
|
// Removes all reminders with no value set.
|
||||||
_reminderDates?.removeWhere((d) => d == null);
|
_reminderDates.removeWhere((d) => d == DateTime(0));
|
||||||
|
|
||||||
Task updatedTask = widget.task.copyWith(
|
Task updatedTask = widget.task.copyWith(
|
||||||
title: _title,
|
title: _title,
|
||||||
|
@ -417,7 +415,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
|
|
||||||
_removeLabel(Label label) {
|
_removeLabel(Label label) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_labels?.removeWhere((l) => l.id == label.id);
|
_labels.removeWhere((l) => l.id == label.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,7 +423,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
return VikunjaGlobal.of(context)
|
return VikunjaGlobal.of(context)
|
||||||
.labelService.getAll(query: query).then((labels) {
|
.labelService.getAll(query: query).then((labels) {
|
||||||
// Only show those labels which aren't already added to the task
|
// Only show those labels which aren't already added to the task
|
||||||
labels.removeWhere((labelToRemove) => _labels!.contains(labelToRemove));
|
labels.removeWhere((labelToRemove) => _labels.contains(labelToRemove));
|
||||||
_suggestedLabels = labels;
|
_suggestedLabels = labels;
|
||||||
List<String?> labelText = labels.map((label) => label.title).toList();
|
List<String?> labelText = labels.map((label) => label.title).toList();
|
||||||
return labelText;
|
return labelText;
|
||||||
|
@ -437,7 +435,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
_suggestedLabels?.forEach((label) {
|
_suggestedLabels?.forEach((label) {
|
||||||
if (label.title == labelTitle) {
|
if (label.title == labelTitle) {
|
||||||
_labels?.add(label);
|
_labels.add(label);
|
||||||
found = true;
|
found = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -448,19 +446,27 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
_createAndAddLabel(String labelTitle) {
|
void _createAndAddLabel(String labelTitle) {
|
||||||
// Only add a label if there are none to add
|
// Only add a label if there are none to add
|
||||||
if (labelTitle.isEmpty || (_suggestedLabels?.isNotEmpty ?? false)) {
|
if (labelTitle.isEmpty || (_suggestedLabels?.isNotEmpty ?? false)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Label newLabel = Label(title: labelTitle, id: 0);
|
final currentUser = VikunjaGlobal.of(context).currentUser;
|
||||||
|
if (currentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final newLabel = Label(
|
||||||
|
title: labelTitle,
|
||||||
|
createdBy: currentUser,
|
||||||
|
);
|
||||||
VikunjaGlobal.of(context)
|
VikunjaGlobal.of(context)
|
||||||
.labelService
|
.labelService
|
||||||
.create(newLabel)
|
.create(newLabel)
|
||||||
.then((createdLabel) {
|
.then((createdLabel) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_labels?.add(createdLabel);
|
_labels.add(createdLabel);
|
||||||
_labelTypeAheadController.clear();
|
_labelTypeAheadController.clear();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -505,7 +511,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onColorEdit() {
|
_onColorEdit() {
|
||||||
_pickerColor = _resetColor
|
_pickerColor = _resetColor || (_color ?? widget.task.color) == null
|
||||||
? Colors.black
|
? Colors.black
|
||||||
: _color ?? widget.task.color;
|
: _color ?? widget.task.color;
|
||||||
showDialog(
|
showDialog(
|
||||||
|
@ -524,11 +530,11 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
),
|
),
|
||||||
actions: <TextButton>[
|
actions: <TextButton>[
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Cancel'),
|
child: Text('CANCEL'),
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Reset'),
|
child: Text('RESET'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
_color = null;
|
_color = null;
|
||||||
|
@ -539,7 +545,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: Text('Ok'),
|
child: Text('OK'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_pickerColor != Colors.black) setState(() {
|
if (_pickerColor != Colors.black) setState(() {
|
||||||
_color = _pickerColor;
|
_color = _pickerColor;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:after_layout/after_layout.dart';
|
import 'package:after_layout/after_layout.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -48,7 +47,7 @@ class _NamespacePageState extends State<NamespacePage>
|
||||||
key: Key(ls.id.toString()),
|
key: Key(ls.id.toString()),
|
||||||
direction: DismissDirection.startToEnd,
|
direction: DismissDirection.startToEnd,
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
title: new Text(ls.title ?? ""),
|
title: new Text(ls.title),
|
||||||
onTap: () => _openList(context, ls),
|
onTap: () => _openList(context, ls),
|
||||||
trailing: Icon(Icons.arrow_right),
|
trailing: Icon(Icons.arrow_right),
|
||||||
),
|
),
|
||||||
|
@ -84,7 +83,7 @@ class _NamespacePageState extends State<NamespacePage>
|
||||||
_loadLists();
|
_loadLists();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _removeList(TaskList list) {
|
Future<void> _removeList(TaskList list) {
|
||||||
return VikunjaGlobal.of(context)
|
return VikunjaGlobal.of(context)
|
||||||
.listService
|
.listService
|
||||||
.delete(list.id)
|
.delete(list.id)
|
||||||
|
@ -124,16 +123,22 @@ class _NamespacePageState extends State<NamespacePage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_addList(String name, BuildContext context) {
|
void _addList(String name, BuildContext context) {
|
||||||
|
final curentUser = VikunjaGlobal.of(context).currentUser;
|
||||||
|
if (curentUser == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
VikunjaGlobal.of(context)
|
VikunjaGlobal.of(context)
|
||||||
.listService
|
.listService
|
||||||
.create(
|
.create(
|
||||||
widget.namespace.id,
|
widget.namespace.id,
|
||||||
TaskList(
|
TaskList(
|
||||||
id: 0,
|
title: name,
|
||||||
title: name,
|
tasks: [],
|
||||||
tasks: [],
|
namespaceId: widget.namespace.id,
|
||||||
namespaceId: widget.namespace.id))
|
owner: curentUser,
|
||||||
|
))
|
||||||
.then((_) {
|
.then((_) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
_loadLists();
|
_loadLists();
|
||||||
|
|
|
@ -16,7 +16,14 @@ class NamespaceEditPage extends StatefulWidget {
|
||||||
class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
bool _loading = false;
|
bool _loading = false;
|
||||||
String? _name, _description;
|
late String _name, _description;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_name = widget.namespace.title;
|
||||||
|
_description = widget.namespace.description;
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) {
|
Widget build(BuildContext ctx) {
|
||||||
|
@ -37,7 +44,7 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
initialValue: widget.namespace.title,
|
initialValue: widget.namespace.title,
|
||||||
onSaved: (name) => _name = name,
|
onSaved: (name) => _name = name ?? '',
|
||||||
validator: (name) {
|
validator: (name) {
|
||||||
//if (name.length < 3 || name.length > 250) {
|
//if (name.length < 3 || name.length > 250) {
|
||||||
// return 'The name needs to have between 3 and 250 characters.';
|
// return 'The name needs to have between 3 and 250 characters.';
|
||||||
|
@ -56,7 +63,7 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
keyboardType: TextInputType.multiline,
|
keyboardType: TextInputType.multiline,
|
||||||
initialValue: widget.namespace.description,
|
initialValue: widget.namespace.description,
|
||||||
onSaved: (description) => _description = description,
|
onSaved: (description) => _description = description ?? '',
|
||||||
validator: (description) {
|
validator: (description) {
|
||||||
//if (description.length > 1000) {
|
//if (description.length > 1000) {
|
||||||
// return 'The description can have a maximum of 1000 characters.';
|
// return 'The description can have a maximum of 1000 characters.';
|
||||||
|
@ -80,7 +87,7 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
||||||
_saveNamespace(context);
|
_saveNamespace(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: () => null,
|
: null,
|
||||||
child: _loading
|
child: _loading
|
||||||
? CircularProgressIndicator()
|
? CircularProgressIndicator()
|
||||||
: VikunjaButtonText('Save'),
|
: VikunjaButtonText('Save'),
|
||||||
|
@ -97,10 +104,13 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
|
||||||
// FIXME: is there a way we can update the namespace without creating a new namespace object?
|
// FIXME: is there a way we can update the namespace without creating a new namespace object?
|
||||||
// aka updating the existing namespace we got from context (setters?)
|
// aka updating the existing namespace we got from context (setters?)
|
||||||
Namespace updatedNamespace = Namespace(
|
Namespace updatedNamespace = Namespace(
|
||||||
id: widget.namespace.id,
|
id: widget.namespace.id,
|
||||||
title: _name,
|
title: _name,
|
||||||
description: _description,
|
description: _description,
|
||||||
owner: widget.namespace.owner);
|
owner: widget.namespace.owner,
|
||||||
|
created: widget.namespace.created,
|
||||||
|
updated: widget.namespace.updated,
|
||||||
|
);
|
||||||
|
|
||||||
VikunjaGlobal.of(context)
|
VikunjaGlobal.of(context)
|
||||||
.namespaceService
|
.namespaceService
|
||||||
|
|
|
@ -32,7 +32,7 @@ class SettingsPageState extends State<SettingsPage> {
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text("Default List"),
|
title: Text("Default List"),
|
||||||
trailing: DropdownButton<int>(
|
trailing: DropdownButton<int>(
|
||||||
items: [DropdownMenuItem(child: Text("None"), value: null,), ...taskListList!.map((e) => DropdownMenuItem(child: Text(e.title ?? ""), value: e.id)).toList()],
|
items: [DropdownMenuItem(child: Text("None"), value: null,), ...taskListList!.map((e) => DropdownMenuItem(child: Text(e.title), value: e.id)).toList()],
|
||||||
value: defaultList,
|
value: defaultList,
|
||||||
onChanged: (int? value){
|
onChanged: (int? value){
|
||||||
setState(() => defaultList = value);
|
setState(() => defaultList = value);
|
||||||
|
|
|
@ -33,28 +33,28 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
Future.delayed(Duration.zero, () {
|
Future.delayed(Duration.zero, () {
|
||||||
if(VikunjaGlobal.of(context).expired) {
|
if(VikunjaGlobal.of(context).expired) {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
.showSnackBar(
|
.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
"Login has expired. Please reenter your details!")));
|
"Login has expired. Please reenter your details!")));
|
||||||
setState(() {
|
setState(() {
|
||||||
_serverController.text = VikunjaGlobal.of(context).client.base ?? "";
|
_serverController.text = VikunjaGlobal.of(context).client.base;
|
||||||
_usernameController.text = VikunjaGlobal.of(context).currentUser?.username ?? "";
|
_usernameController.text = VikunjaGlobal.of(context).currentUser?.username ?? "";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
final client = VikunjaGlobal.of(context).client;
|
||||||
|
VikunjaGlobal.of(context).settingsManager.getIgnoreCertificates().then((value) => setState(() => client.ignoreCertificates = value == "1"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) {
|
Widget build(BuildContext ctx) {
|
||||||
Client client = VikunjaGlobal.of(context).client;
|
Client client = VikunjaGlobal.of(context).client;
|
||||||
if(client.ignoreCertificates == null)
|
|
||||||
VikunjaGlobal.of(context).settingsManager.getIgnoreCertificates().then((value) => setState(() => client.ignoreCertificates = value == "1" ? true:false));
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: Center(
|
body: Center(
|
||||||
|
@ -131,7 +131,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
_loginUser(context);
|
_loginUser(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: () => null,
|
: null,
|
||||||
child: _loading
|
child: _loading
|
||||||
? CircularProgressIndicator()
|
? CircularProgressIndicator()
|
||||||
: VikunjaButtonText('Login'),
|
: VikunjaButtonText('Login'),
|
||||||
|
|
|
@ -8,7 +8,7 @@ import 'package:vikunja_app/models/user.dart';
|
||||||
import 'package:vikunja_app/service/services.dart';
|
import 'package:vikunja_app/service/services.dart';
|
||||||
|
|
||||||
// Data for mocked services
|
// Data for mocked services
|
||||||
var _users = {1: User(1, 'test@testuser.org', 'test1')};
|
var _users = {1: User(id: 1, username: 'test1')};
|
||||||
|
|
||||||
var _namespaces = {
|
var _namespaces = {
|
||||||
1: Namespace(
|
1: Namespace(
|
||||||
|
@ -17,7 +17,7 @@ var _namespaces = {
|
||||||
created: DateTime.now(),
|
created: DateTime.now(),
|
||||||
updated: DateTime.now(),
|
updated: DateTime.now(),
|
||||||
description: 'A namespace for testing purposes',
|
description: 'A namespace for testing purposes',
|
||||||
owner: _users[1],
|
owner: _users[1]!,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ var _lists = {
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'List 1',
|
title: 'List 1',
|
||||||
tasks: _tasks.values.toList(),
|
tasks: _tasks.values.toList(),
|
||||||
owner: _users[1],
|
owner: _users[1]!,
|
||||||
description: 'A nice list',
|
description: 'A nice list',
|
||||||
created: DateTime.now(),
|
created: DateTime.now(),
|
||||||
updated: DateTime.now(),
|
updated: DateTime.now(),
|
||||||
|
@ -41,12 +41,12 @@ var _tasks = {
|
||||||
1: Task(
|
1: Task(
|
||||||
id: 1,
|
id: 1,
|
||||||
title: 'Task 1',
|
title: 'Task 1',
|
||||||
createdBy: _users[1],
|
createdBy: _users[1]!,
|
||||||
updated: DateTime.now(),
|
updated: DateTime.now(),
|
||||||
created: DateTime.now(),
|
created: DateTime.now(),
|
||||||
description: 'A descriptive task',
|
description: 'A descriptive task',
|
||||||
done: false,
|
done: false,
|
||||||
identifier: '',
|
listId: 1,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ class MockedTaskService implements TaskService {
|
||||||
@override
|
@override
|
||||||
Future delete(int taskId) {
|
Future delete(int taskId) {
|
||||||
_lists.forEach(
|
_lists.forEach(
|
||||||
(_, list) => list.tasks.removeWhere((task) => task?.id == taskId));
|
(_, list) => list.tasks.removeWhere((task) => task.id == taskId));
|
||||||
_tasks.remove(taskId);
|
_tasks.remove(taskId);
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
@ -154,12 +154,12 @@ class MockedTaskService implements TaskService {
|
||||||
@override
|
@override
|
||||||
Future<Task> update(Task task) {
|
Future<Task> update(Task task) {
|
||||||
_lists.forEach((_, list) {
|
_lists.forEach((_, list) {
|
||||||
if (list.tasks.where((t) => t?.id == task.id).length > 0) {
|
if (list.tasks.where((t) => t.id == task.id).length > 0) {
|
||||||
list.tasks.removeWhere((t) => t?.id == task.id);
|
list.tasks.removeWhere((t) => t.id == task.id);
|
||||||
list.tasks.add(task);
|
list.tasks.add(task);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return Future.value(_tasks[task.id ?? 0] = task);
|
return Future.value(_tasks[task.id] = task);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
|
@ -87,14 +87,17 @@ class ListProvider with ChangeNotifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTaskByTitle(
|
Future<void> addTaskByTitle(
|
||||||
{required BuildContext context, required String title, required int listId}) {
|
{required BuildContext context, required String title, required int listId}) async{
|
||||||
var globalState = VikunjaGlobal.of(context);
|
final globalState = VikunjaGlobal.of(context);
|
||||||
var newTask = Task(
|
if (globalState.currentUser == null) {
|
||||||
id: 0,
|
return;
|
||||||
identifier: '',
|
}
|
||||||
|
|
||||||
|
final newTask = Task(
|
||||||
title: title,
|
title: title,
|
||||||
createdBy: globalState.currentUser,
|
createdBy: globalState.currentUser!,
|
||||||
done: false,
|
done: false,
|
||||||
|
listId: listId,
|
||||||
);
|
);
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -116,11 +119,7 @@ class ListProvider with ChangeNotifier {
|
||||||
_tasks.insert(0, task);
|
_tasks.insert(0, task);
|
||||||
if (_buckets.isNotEmpty) {
|
if (_buckets.isNotEmpty) {
|
||||||
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
|
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
|
||||||
if (bucket.tasks != null) {
|
bucket.tasks.add(task);
|
||||||
bucket.tasks.add(task);
|
|
||||||
} else {
|
|
||||||
bucket.tasks = <Task>[task];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_isLoading = false;
|
_isLoading = false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
@ -159,7 +158,7 @@ class ListProvider with ChangeNotifier {
|
||||||
return VikunjaGlobal.of(context).bucketService.update(bucket)
|
return VikunjaGlobal.of(context).bucketService.update(bucket)
|
||||||
.then((rBucket) {
|
.then((rBucket) {
|
||||||
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
|
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
|
||||||
_buckets.sort((a, b) => a.position.compareTo(b.position));
|
_buckets.sort((a, b) => a.position!.compareTo(b.position!));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -172,7 +171,7 @@ class ListProvider with ChangeNotifier {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> moveTaskToBucket({required BuildContext context, required Task task, required int newBucketId, required int index}) async {
|
Future<void> moveTaskToBucket({required BuildContext context, required Task task, int? newBucketId, required int index}) async {
|
||||||
final sameBucket = task.bucketId == newBucketId;
|
final sameBucket = task.bucketId == newBucketId;
|
||||||
final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId);
|
final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId);
|
||||||
if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)) index--;
|
if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)) index--;
|
||||||
|
@ -212,7 +211,7 @@ class ListProvider with ChangeNotifier {
|
||||||
if (_tasks.isNotEmpty) {
|
if (_tasks.isNotEmpty) {
|
||||||
_tasks[_tasks.indexWhere((t) => t.id == task.id)] = task;
|
_tasks[_tasks.indexWhere((t) => t.id == task.id)] = task;
|
||||||
if (secondTask != null)
|
if (secondTask != null)
|
||||||
_tasks[_tasks.indexWhere((t) => t.id == secondTask?.id)] = secondTask;
|
_tasks[_tasks.indexWhere((t) => t.id == secondTask!.id)] = secondTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
_buckets[newBucketIndex].tasks[_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)] = task;
|
_buckets[newBucketIndex].tasks[_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)] = task;
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import 'package:meta/meta.dart';
|
|
||||||
|
|
||||||
class CheckboxStatistics {
|
class CheckboxStatistics {
|
||||||
final int total;
|
final int total;
|
||||||
final int checked;
|
final int checked;
|
||||||
|
@ -28,7 +26,7 @@ MatchedCheckboxes getCheckboxesInText(String text) {
|
||||||
final matches = RegExp(r'[*-] \[[ x]]').allMatches(text);
|
final matches = RegExp(r'[*-] \[[ x]]').allMatches(text);
|
||||||
|
|
||||||
for (final match in matches) {
|
for (final match in matches) {
|
||||||
if (match[0]!.endsWith(checkedString))
|
if (match[0]?.endsWith(checkedString) ?? false)
|
||||||
checked.add(match);
|
checked.add(match);
|
||||||
else
|
else
|
||||||
unchecked.add(match);
|
unchecked.add(match);
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
final RegExp _emailRegex = new RegExp(
|
final RegExp _emailRegex = new RegExp(
|
||||||
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$');
|
r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$');
|
||||||
|
|
||||||
bool isEmail(email) {
|
bool isEmail(String? email) {
|
||||||
return _emailRegex.hasMatch(email);
|
return _emailRegex.hasMatch(email ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
final RegExp _url = new RegExp(
|
final RegExp _url = new RegExp(
|
||||||
r'https?:\/\/((([a-zA-Z0-9.\-\_]+)\.[a-zA-Z]+)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?');
|
r'https?:\/\/((([a-zA-Z0-9.\-\_]+)\.[a-zA-Z]+)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?');
|
||||||
|
|
||||||
bool isUrl(url) {
|
bool isUrl(String? url) {
|
||||||
return _url.hasMatch(url);
|
return _url.hasMatch(url ?? '');
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:vikunja_app/models/label.dart';
|
import 'package:vikunja_app/models/label.dart';
|
||||||
|
import 'package:vikunja_app/models/user.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('label color from json', () {
|
test('label color from json', () {
|
||||||
|
@ -14,9 +15,9 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('hex color string from object', () {
|
test('hex color string from object', () {
|
||||||
Label label = Label(id: 1, color: Color(0xFFe8e8e8));
|
Label label = Label(id: 1, title: '', color: Color(0xFFe8e8e8), createdBy: User(id: 0, username: ''));
|
||||||
var json = label.toJSON();
|
var json = label.toJSON();
|
||||||
|
|
||||||
expect(json.toString(), '{id: 1, title: null, description: null, hex_color: e8e8e8, created_by: null, updated: null, created: null}');
|
expect(json.toString(), '{id: 1, title: , description: null, hex_color: e8e8e8, created_by: {id: 0, username: ,}, updated: null, created: null}');
|
||||||
});
|
});
|
||||||
}
|
}
|
Loading…
Reference in New Issue