null-safety & some other cleanup

This commit is contained in:
Paul Nettleton 2022-09-03 10:43:16 -05:00
parent 9161e1fa12
commit d4f234d65c
31 changed files with 570 additions and 531 deletions

6
.vscode/launch.json vendored
View File

@ -4,7 +4,11 @@
"name": "Flutter",
"request": "launch",
"type": "dart",
"flutterMode": "debug"
"flutterMode": "debug",
"args": [
"--flavor",
"main"
],
}
]
}

View File

@ -16,13 +16,13 @@ class Client {
GlobalKey<ScaffoldMessengerState> global;
final JsonDecoder _decoder = new JsonDecoder();
final JsonEncoder _encoder = new JsonEncoder();
String? _token;
String? _base;
String _token = '';
String _base = '';
bool authenticated = false;
bool ignoreCertificates = false;
String? get base => _base;
String? get token => _token;
String get base => _base;
String get token => _token;
String? post_body;
@ -45,7 +45,7 @@ class Client {
get _headers =>
{
'Authorization': _token != null ? 'Bearer $_token' : '',
'Authorization': _token != '' ? 'Bearer $_token' : '',
'Content-Type': 'application/json'
};
@ -63,26 +63,14 @@ class Client {
void reset() {
_token = _base = null;
_token = _base = '';
authenticated = false;
}
Future<Response> get(String url,
[Map<String, List<String>>? queryParameters]) {
// TODO: This could be moved to a seperate function
var uri = Uri.parse('${this.base}$url');
// 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)
final uri = Uri.parse('${this.base}$url').replace(queryParameters: queryParameters);
return http.get(uri, headers: _headers)
.then(_handleResponse, onError: _handleError);
}

View File

@ -29,8 +29,8 @@ class LabelAPIService extends APIService implements LabelService {
@override
Future<List<Label>> getAll({String? query}) {
String? params =
query == null ? null : '?s=' + Uri.encodeQueryComponent(query);
String params =
query == null ? '' : '?s=' + Uri.encodeQueryComponent(query);
return client.get('/labels$params').then(
(response) => convertList(response.body, (result) => Label.fromJson(result)));
}

View File

@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/components/datetimePicker.dart';
import 'package:vikunja_app/global.dart';
import 'dart:developer';
import '../models/task.dart';
enum NewTaskDue {day,week, month, custom}
// TODO: add to enum above
Map<NewTaskDue, Duration> newTaskDueToDuration = {
NewTaskDue.day: Duration(days: 1),
NewTaskDue.week: Duration(days: 7),
@ -12,7 +14,7 @@ Map<NewTaskDue, Duration> newTaskDueToDuration = {
class AddDialog extends StatefulWidget {
final ValueChanged<String>? onAdd;
final ValueChanged<Task>? onAddTask;
final void Function(String title, DateTime? dueDate)? onAddTask;
final InputDecoration? decoration;
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)
widget.onAdd!(textController.text);
if(widget.onAddTask != null && textController.text.isNotEmpty) {
widget.onAddTask!(Task(id: 0,
title: textController.text,
done: false,
createdBy: null,
dueDate: customDueDate, identifier: ''));
widget.onAddTask!(textController.text, customDueDate);
}
Navigator.pop(context);
},

View File

@ -30,11 +30,7 @@ class BucketTaskCard extends StatefulWidget {
required this.index,
required this.onDragUpdate,
required this.onAccept,
}) : assert(task != null),
assert(index != null),
assert(onDragUpdate != null),
assert(onAccept != null),
super(key: key);
}) : super(key: key);
@override
State<BucketTaskCard> createState() => _BucketTaskCardState();
@ -61,9 +57,9 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
final identifierRow = Row(
children: <Widget>[
Text(
widget.task.identifier.isNotEmpty
? '#${widget.task.identifier.substring(1)}' : '${widget.task.id}',
style: theme.textTheme.subtitle2?.copyWith(
(widget.task.identifier?.isNotEmpty ?? false)
? '#${widget.task.identifier!.substring(1)}' : '${widget.task.id}',
style: (theme.textTheme.subtitle2 ?? TextStyle()).copyWith(
color: Colors.grey,
),
),
@ -76,7 +72,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
child: FittedBox(
child: Chip(
label: Text('Done'),
labelStyle: theme.textTheme.labelLarge?.copyWith(
labelStyle: (theme.textTheme.labelLarge ?? TextStyle()).copyWith(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.black : Colors.white,
@ -91,8 +87,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
children: <Widget>[
Expanded(
child: Text(
widget.task.title ?? "",
style: theme.textTheme.titleMedium?.copyWith(
widget.task.title,
style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16)).copyWith(
color: widget.task.textColor,
),
),
@ -112,7 +108,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
color: pastDue ? Colors.red : null,
),
label: Text(durationToHumanReadable(duration)),
labelStyle: theme.textTheme.labelLarge?.copyWith(
labelStyle: (theme.textTheme.labelLarge ?? TextStyle()).copyWith(
color: pastDue ? Colors.red : null,
),
backgroundColor: pastDue ? Colors.red.withAlpha(20) : null,
@ -126,10 +122,10 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
spacing: 4,
runSpacing: 4,
);
widget.task.labels?.sort((a, b) => a.title?.compareTo(b.title ?? "") ?? 0);
widget.task.labels?.asMap().forEach((i, label) {
widget.task.labels.sort((a, b) => a.title.compareTo(b.title));
widget.task.labels.asMap().forEach((i, label) {
labelRow.children.add(Chip(
label: Text(label.title ?? ""),
label: Text(label.title),
labelStyle: theme.textTheme.labelLarge?.copyWith(
color: label.textColor,
),
@ -137,7 +133,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
));
});
if (widget.task.hasCheckboxes) {
final checkboxStatistics = widget.task.checkboxStatistics!;
final checkboxStatistics = widget.task.checkboxStatistics;
final iconSize = (theme.textTheme.labelLarge?.fontSize ?? 14) + 2;
labelRow.children.add(Chip(
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(
label: Transform.rotate(
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(
label: Icon(Icons.notes),
));
@ -237,7 +233,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
child: () {
if (_dragging || _cardSize == null) return card;
final dropBoxSize = _dropData?.size ?? _cardSize;
final cardSize = _cardSize!;
final dropBoxSize = _dropData?.size ?? cardSize;
final dropBox = DottedBorder(
color: Colors.grey,
child: SizedBox.fromSize(size: dropBoxSize),
@ -268,8 +265,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
};
return SizedBox(
width: _cardSize!.width,
height: _cardSize!.height + (dropAbove || dropBelow ? dropBoxSize!.height + 4 : 0),
width: cardSize.width,
height: cardSize.height + (dropAbove || dropBelow ? dropBoxSize.height + 4 : 0),
child: Stack(
children: <Widget>[
Column(
@ -282,7 +279,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
Column(
children: <SizedBox>[
SizedBox(
height: (_cardSize!.height / 2) + (dropAbove ? dropBoxSize!.height : 0),
height: (cardSize.height / 2) + (dropAbove ? dropBoxSize.height : 0),
child: DragTarget<TaskData>(
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.above),
onAccept: dragTargetOnAccept,
@ -291,7 +288,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
),
),
SizedBox(
height: (_cardSize!.height / 2) + (dropBelow ? dropBoxSize!.height : 0),
height: (cardSize.height / 2) + (dropBelow ? dropBoxSize.height : 0),
child: DragTarget<TaskData>(
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.below),
onAccept: dragTargetOnAccept,

View File

@ -19,7 +19,6 @@ class SliverBucketList extends StatelessWidget {
Widget build(BuildContext context) {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (bucket.tasks == null) return null;
return index >= bucket.tasks.length ? null : BucketTaskCard(
key: ObjectKey(bucket.tasks[index]),
task: bucket.tasks[index],
@ -33,14 +32,16 @@ class SliverBucketList extends StatelessWidget {
);
}
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) {
return Provider.of<ListProvider>(context, listen: false).moveTaskToBucket(
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) async {
await Provider.of<ListProvider>(context, listen: false).moveTaskToBucket(
context: context,
task: task,
newBucketId: bucket.id,
index: index,
).then((_) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('${task.title} was moved to ${bucket.title} successfully!'),
)));
));
}
}

View File

@ -14,9 +14,14 @@ class TaskTile extends StatefulWidget {
final bool loading;
final ValueSetter<bool>? onMarkedAsDone;
const TaskTile(
{Key? key, required this.task, required this.onEdit, this.loading = false, this.showInfo = false, this.onMarkedAsDone})
: super(key: key);
const TaskTile({
Key? key,
required this.task,
required this.onEdit,
this.loading = false,
this.showInfo = false,
this.onMarkedAsDone,
}) : super(key: key);
/*
@override
TaskTileState createState() {
@ -49,11 +54,11 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
strokeWidth: 2.0,
)),
),
title: Text(_currentTask.title ?? ""),
title: Text(_currentTask.title),
subtitle:
_currentTask.description == null || _currentTask.description!.isEmpty
_currentTask.description.isEmpty
? null
: Text(_currentTask.description ?? ""),
: Text(_currentTask.description),
trailing: IconButton(
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,
),
)
) : Text(_currentTask.title ?? ""),
) : Text(_currentTask.title),
controlAffinity: ListTileControlAffinity.leading,
value: _currentTask.done,
subtitle: widget.showInfo && _currentTask.hasDueDate ?
Text("Due " + durationToHumanReadable(durationUntilDue!), style: TextStyle(color: durationUntilDue.isNegative ? Colors.red : null),)
: _currentTask.description == null || _currentTask.description!.isEmpty
Text("Due " + durationToHumanReadable(durationUntilDue!), style: durationUntilDue.isNegative ? TextStyle(color: Colors.red) : null,)
: _currentTask.description.isEmpty
? null
: Text(_currentTask.description ?? ""),
: Text(_currentTask.description),
secondary:
IconButton(icon: Icon(Icons.settings), onPressed: () {
Navigator.push<Task>(

View File

@ -1,40 +1,28 @@
import 'package:flutter/material.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 VoidCallback onDelete;
const LabelComponent({Key? key, required this.label, required this.onDelete})
: super(key: key);
@override
State<StatefulWidget> createState() {
return new LabelComponentState();
}
}
class LabelComponentState extends State<LabelComponent> {
@override
Widget build(BuildContext context) {
Color backgroundColor = widget.label.color;
Color textColor =
backgroundColor.computeLuminance() > 0.5 ? vLabelDark : vLabelLight;
return Chip(
label: Text(
widget.label.title ?? "",
label.title,
style: TextStyle(
color: textColor,
color: label.textColor,
),
),
backgroundColor: backgroundColor,
backgroundColor: label.color,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(3)),
),
onDeleted: widget.onDelete,
deleteIconColor: textColor,
onDeleted: onDelete,
deleteIconColor: label.textColor,
deleteIcon: Container(
padding: EdgeInsets.all(3),
decoration: BoxDecoration(
@ -43,7 +31,7 @@ class LabelComponentState extends State<LabelComponent> {
),
child: Icon(
Icons.close,
color: textColor,
color: label.textColor,
size: 15,
),
),

View File

@ -158,29 +158,33 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
requestIOSPermissions(notificationsPlugin);
}
void scheduleDueNotifications() {
notificationsPlugin.cancelAll().then((value) {
taskService.getAll().then((value) =>
value.forEach((task) {
if(task.reminderDates != null)
task.reminderDates!.forEach((reminder) {
scheduleNotification("Reminder", "This is your reminder for '" + task.title! + "'",
notificationsPlugin,
reminder!,
currentTimeZone,
platformChannelSpecificsReminders,
id: (reminder.millisecondsSinceEpoch/1000).floor());
});
if(task.dueDate != null)
scheduleNotification("Due Reminder","The task '" + task.title! + "' is due.",
notificationsPlugin,
task.dueDate!,
currentTimeZone,
platformChannelSpecificsDueDate,
id: task.id);
})
);
});
Future<void> scheduleDueNotifications() async {
await notificationsPlugin.cancelAll();
final tasks = await taskService.getAll();
for (final task in tasks) {
for (final reminder in task.reminderDates) {
scheduleNotification(
"Reminder",
"This is your reminder for '" + task.title + "'",
notificationsPlugin,
reminder,
currentTimeZone,
platformChannelSpecificsReminders,
id: (reminder.millisecondsSinceEpoch / 1000).floor(),
);
}
if (task.hasDueDate) {
scheduleNotification(
"Due Reminder",
"The task '" + task.title + "' is due.",
notificationsPlugin,
task.dueDate!,
currentTimeZone,
platformChannelSpecificsDueDate,
id: task.id,
);
}
}
}
@ -215,7 +219,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
return;
}
client.configure(token: token, base: base, authenticated: true);
var loadedCurrentUser;
User loadedCurrentUser;
try {
loadedCurrentUser = await UserAPIService(client).getCurrentUser();
} on ApiException catch (e) {
@ -233,9 +237,9 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
});
return;
}
loadedCurrentUser = User(int.tryParse(currentUser)!, "", "");
loadedCurrentUser = User(id: int.parse(currentUser), username: '');
} catch (otherExceptions) {
loadedCurrentUser = User(int.tryParse(currentUser)!, "", "");
loadedCurrentUser = User(id: int.parse(currentUser), username: '');
}
setState(() {
_currentUser = loadedCurrentUser;

View File

@ -1,4 +1,3 @@
import 'package:meta/meta.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:vikunja_app/models/task.dart';
@ -7,55 +6,58 @@ import 'package:vikunja_app/models/user.dart';
@JsonSerializable()
class Bucket {
int id, listId, limit;
String? title;
double position;
DateTime? created, updated;
User? createdBy;
String title;
double? position;
late final DateTime created, updated;
User createdBy;
bool isDoneBucket;
late final List<Task> tasks;
Bucket({
required this.id,
this.id = -1,
required this.listId,
this.title,
this.position = 0,
required this.title,
this.position,
required this.limit,
this.isDoneBucket = false,
this.created,
this.updated,
this.createdBy,
this.tasks = const <Task>[],
});
List<Task> tasks = [];
DateTime? created,
DateTime? updated,
required this.createdBy,
List<Task>? tasks,
}) {
this.created = created ?? DateTime.now();
this.updated = created ?? DateTime.now();
this.tasks = tasks ?? [];
}
Bucket.fromJSON(Map<String, dynamic> json)
: id = json['id'],
listId = json['list_id'],
title = json['title'],
position = json['position'] is int
? json['position'].toDouble()
: json['position'],
limit = json['limit'],
isDoneBucket = json['is_done_bucket'],
created = DateTime.parse(json['created']),
updated = DateTime.parse(json['updated']),
createdBy = json['created_by'] == null
? null
: User.fromJson(json['created_by']),
tasks = (((json['tasks'] == null) ? [] : json['tasks']) as List<dynamic>)
.map((task) => Task.fromJson(task))
.cast<Task>()
.toList();
: id = json['id'],
listId = json['list_id'],
title = json['title'],
position = json['position'] is int
? json['position'].toDouble()
: json['position'],
limit = json['limit'],
isDoneBucket = json['is_done_bucket'],
created = DateTime.parse(json['created']),
updated = DateTime.parse(json['updated']),
createdBy = User.fromJson(json['created_by']),
tasks = json['tasks'] == null
? []
: (json['tasks'] as List<dynamic>)
.map((task) => Task.fromJson(task))
.toList();
toJSON() => {
'id': id,
'id': id != -1 ? id : null,
'list_id': listId,
'title': title,
'position': position,
'limit': limit,
'is_done_bucket': isDoneBucket,
'created': created?.toUtc().toIso8601String(),
'updated': updated?.toUtc().toIso8601String(),
'createdBy': createdBy?.toJSON(),
'created': created.toUtc().toIso8601String(),
'updated': updated.toUtc().toIso8601String(),
'created_by': createdBy.toJSON(),
'tasks': tasks.map((task) => task.toJSON()).toList(),
};
}

View File

@ -5,42 +5,45 @@ import 'package:vikunja_app/theme/constants.dart';
class Label {
final int id;
final String? title, description;
final DateTime? created, updated;
final User? createdBy;
final Color color;
final String title, description;
late final DateTime created, updated;
final User createdBy;
final Color? color;
Label(
{
required this.id,
this.title,
this.description,
this.color = vLabelDefaultColor,
this.created,
this.updated,
this.createdBy});
late final Color textColor = color != null && color!.computeLuminance() <= 0.5 ? vLabelLight : vLabelDark;
Label({
this.id = -1,
required this.title,
this.description = '',
this.color,
DateTime? created,
DateTime? updated,
required this.createdBy,
}) {
this.created = created ?? DateTime.now();
this.updated = updated ?? DateTime.now();
}
Label.fromJson(Map<String, dynamic> json)
: id = json['id'],
title = json['title'],
description = json['description'],
color = json['hex_color'] == ''
? vLabelDefaultColor
? null
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
createdBy = User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'id': id != -1 ? id : null,
'title': title,
'description': description,
'hex_color':
color.value.toRadixString(16).padLeft(8, '0').substring(2),
'created_by': createdBy?.toJSON(),
'updated': updated?.toUtc().toIso8601String(),
'created': created?.toUtc().toIso8601String(),
color?.value.toRadixString(16).padLeft(8, '0').substring(2),
'created_by': createdBy.toJSON(),
'updated': updated.toUtc().toIso8601String(),
'created': created.toUtc().toIso8601String(),
};
Color get textColor => color != null && color.computeLuminance() <= 0.5 ? vLabelLight : vLabelDark;
}

View File

@ -1,6 +1,7 @@
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';
class LabelTask {
final Label label;
@ -8,8 +9,8 @@ class LabelTask {
LabelTask({required this.label, required this.task});
LabelTask.fromJson(Map<String, dynamic> json)
: label = new Label(id: json['label_id']),
LabelTask.fromJson(Map<String, dynamic> json, User createdBy)
: label = new Label(id: json['label_id'], title: '', createdBy: createdBy),
task = null;
toJSON() => {

View File

@ -1,50 +1,53 @@
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';
class TaskList {
final int id;
int namespaceId;
String? title, description;
final User? owner;
final DateTime? created, updated;
List<Task?> tasks;
String title, description;
final User owner;
late final DateTime created, updated;
late final List<Task> tasks;
final bool isFavorite;
TaskList({
required this.id,
this.id = -1,
required this.title,
required this.namespaceId,
this.description,
this.owner,
this.created,
this.updated,
this.tasks = const <Task>[],
this.description = '',
required this.owner,
DateTime? created,
DateTime? updated,
List<Task>? tasks,
this.isFavorite = false,
});
}) {
this.created = created ?? DateTime.now();
this.updated = updated ?? DateTime.now();
this.tasks = tasks ?? [];
}
TaskList.fromJson(Map<String, dynamic> json)
: id = json['id'],
owner = json['owner'] == null ? null : User.fromJson(json['owner']),
owner = User.fromJson(json['owner']),
description = json['description'],
title = json['title'],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
isFavorite = json['is_favorite'],
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))
.toList();
toJSON() {
return {
"id": this.id,
"title": this.title,
"description": this.description,
"owner": this.owner?.toJSON(),
"created": this.created?.toIso8601String(),
"updated": this.updated?.toIso8601String(),
"namespace_id": this.namespaceId
'id': id != -1 ? id : null,
'title': title,
'description': description,
'owner': owner.toJSON(),
'created': created.toIso8601String(),
'updated': updated.toIso8601String(),
'namespace_id': namespaceId
};
}
}

View File

@ -1,19 +1,22 @@
import 'package:vikunja_app/models/user.dart';
import 'package:meta/meta.dart';
class Namespace {
final int id;
final DateTime? created, updated;
final String? title, description;
final User? owner;
late final DateTime created, updated;
final String title, description;
final User owner;
Namespace(
{required this.id,
this.created,
this.updated,
required this.title,
this.description,
this.owner});
Namespace({
this.id = -1,
DateTime? created,
DateTime? updated,
required this.title,
this.description = '',
required this.owner,
}) {
this.created = created ?? DateTime.now();
this.updated = updated ?? DateTime.now();
}
Namespace.fromJson(Map<String, dynamic> json)
: title = json['title'],
@ -21,13 +24,14 @@ class Namespace {
id = json['id'],
created = DateTime.parse(json['created']),
updated = DateTime.parse(json['updated']),
owner = json['owner'] == null ? null : User.fromJson(json['owner']);
owner = User.fromJson(json['owner']);
toJSON() => {
"created": created?.toIso8601String(),
"updated": updated?.toIso8601String(),
"title": title,
"owner": owner?.toJSON(),
"description": description
'id': id != -1 ? id : null,
'created': created.toIso8601String(),
'updated': updated.toIso8601String(),
'title': title,
'owner': owner.toJSON(),
'description': description
};
}

View File

@ -1,57 +1,63 @@
import 'package:flutter/material.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/user.dart';
import 'package:vikunja_app/models/taskAttachment.dart';
import 'package:vikunja_app/theme/constants.dart';
import 'package:vikunja_app/utils/checkboxes_in_text.dart';
@JsonSerializable()
class Task {
final int? id, parentTaskId, priority, listId, bucketId;
final DateTime? created, updated, dueDate, startDate, endDate;
final List<DateTime?>? reminderDates;
final String identifier;
final String? title, description;
final int id;
final int? parentTaskId, priority, bucketId;
final int listId;
late final DateTime created, updated;
final DateTime? dueDate, startDate, endDate;
final List<DateTime> reminderDates;
final String? identifier;
final String title, description;
final bool done;
final Color color;
final Color? color;
final double? kanbanPosition;
final User? createdBy;
final User createdBy;
final Duration? repeatAfter;
final List<Task>? subtasks;
final List<Label>? labels;
final List<TaskAttachment>? attachments;
final List<Task> subtasks;
final List<Label> labels;
final List<TaskAttachment> attachments;
// 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({
required this.id,
required this.identifier,
this.title,
this.description,
this.id = -1,
this.identifier,
this.title = '',
this.description = '',
this.done = false,
this.reminderDates,
this.reminderDates = const [],
this.dueDate,
this.startDate,
this.endDate,
this.parentTaskId,
this.priority,
this.repeatAfter,
this.color = vBlue, // TODO: decide on color
this.color,
this.kanbanPosition,
this.subtasks,
this.labels,
this.attachments,
this.created,
this.updated,
this.createdBy,
this.listId,
this.subtasks = const [],
this.labels = const [],
this.attachments = const [],
DateTime? created,
DateTime? updated,
required this.createdBy,
required this.listId,
this.bucketId,
});
}) {
this.created = DateTime.now();
this.updated = DateTime.now();
}
bool loading = false;
@ -61,93 +67,85 @@ class Task {
description = json['description'],
identifier = json['identifier'],
done = json['done'],
reminderDates = json['reminder_dates'] != null ? (json['reminder_dates'] as List<dynamic>)
.map((ts) => DateTime.parse(ts))
.cast<DateTime>()
.toList() : null,
reminderDates = json['reminder_dates'] != null
? (json['reminder_dates'] as List<dynamic>)
.map((ts) => DateTime.parse(ts))
.toList()
: [],
dueDate = DateTime.parse(json['due_date']),
startDate = DateTime.parse(json['start_date']),
endDate = DateTime.parse(json['end_date']),
parentTaskId = json['parent_task_id'],
priority = json['priority'],
repeatAfter = Duration(seconds: json['repeat_after']),
color = json['hex_color'] == ''
? vBlue
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
color = json['hex_color'] != ''
? Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000)
: null,
kanbanPosition = json['kanban_position'] is int
? json['kanban_position'].toDouble()
: json['kanban_position'],
labels = ((json['labels'] ?? []) as List<dynamic>)
.map((label) => Label.fromJson(label))
.cast<Label>()
.toList(),
subtasks = ((json['subtasks'] ?? []) as List<dynamic>)
.map((subtask) => Task.fromJson(subtask))
.cast<Task>()
.toList(),
attachments = ((json['attachments'] ?? []) as List<dynamic>)
.map((attachment) => TaskAttachment.fromJSON(attachment))
.cast<TaskAttachment>()
.toList(),
labels = json['labels'] != null
? (json['labels'] as List<dynamic>)
.map((label) => Label.fromJson(label))
.toList()
: [],
subtasks = json['subtasks'] != null
? (json['subtasks'] as List<dynamic>)
.map((subtask) => Task.fromJson(subtask))
.toList()
: [],
attachments = json['attachments'] != null
? (json['attachments'] as List<dynamic>)
.map((attachment) => TaskAttachment.fromJSON(attachment))
.toList()
: [],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
listId = json['list_id'],
bucketId = json['bucket_id'],
createdBy = json['created_by'] == null
? null
: User.fromJson(json['created_by']);
createdBy = User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'id': id != -1 ? id : null,
'title': title,
'description': description,
'identifier': identifier,
'done': done,
'reminder_dates':
reminderDates?.map((date) => date?.toUtc().toIso8601String()).toList(),
'reminder_dates': reminderDates
.map((date) => date.toUtc().toIso8601String())
.toList(),
'due_date': dueDate?.toUtc().toIso8601String(),
'start_date': startDate?.toUtc().toIso8601String(),
'end_date': endDate?.toUtc().toIso8601String(),
'priority': priority,
'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,
'labels': labels?.map((label) => label.toJSON()).toList(),
'subtasks': subtasks?.map((subtask) => subtask.toJSON()).toList(),
'attachments': attachments?.map((attachment) => attachment.toJSON()).toList(),
'labels': labels.map((label) => label.toJSON()).toList(),
'subtasks': subtasks.map((subtask) => subtask.toJSON()).toList(),
'attachments':
attachments.map((attachment) => attachment.toJSON()).toList(),
'bucket_id': bucketId,
'created_by': createdBy?.toJSON(),
'updated': updated?.toUtc().toIso8601String(),
'created': created?.toUtc().toIso8601String(),
'created_by': createdBy.toJSON(),
'updated': updated.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({
int? id, int? parentTaskId, int? priority, int? listId, int? bucketId,
DateTime? created, DateTime? updated, DateTime? dueDate, DateTime? startDate, DateTime? endDate,
List<DateTime?>? reminderDates,
String? title, String? description, String? identifier,
int? id,
int? parentTaskId,
int? priority,
int? listId,
int? bucketId,
DateTime? created,
DateTime? updated,
DateTime? dueDate,
DateTime? startDate,
DateTime? endDate,
List<DateTime>? reminderDates,
String? title,
String? description,
String? identifier,
bool? done,
Color? color,
bool? resetColor,
@ -160,27 +158,27 @@ class Task {
}) {
return Task(
id: id ?? this.id,
parentTaskId: parentTaskId,
priority: priority,
listId: listId,
bucketId: bucketId,
created: created,
updated: updated,
dueDate: dueDate,
startDate: startDate,
endDate: endDate,
reminderDates: reminderDates,
title: title,
description: description,
parentTaskId: parentTaskId ?? this.parentTaskId,
priority: priority ?? this.priority,
listId: listId ?? this.listId,
bucketId: bucketId ?? this.bucketId,
created: created ?? this.created,
updated: updated ?? this.updated,
dueDate: dueDate ?? this.dueDate,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
reminderDates: reminderDates ?? this.reminderDates,
title: title ?? this.title,
description: description ?? this.description,
identifier: identifier ?? this.identifier,
done: done ?? this.done,
color: (resetColor ?? false) ? vBlue : (color ?? this.color),
kanbanPosition: kanbanPosition,
createdBy: createdBy,
repeatAfter: repeatAfter,
subtasks: subtasks,
labels: labels,
attachments: attachments,
color: (resetColor ?? false) ? null : (color ?? this.color),
kanbanPosition: kanbanPosition ?? this.kanbanPosition,
createdBy: createdBy ?? this.createdBy,
repeatAfter: repeatAfter ?? this.repeatAfter,
subtasks: subtasks ?? this.subtasks,
labels: labels ?? this.labels,
attachments: attachments ?? this.attachments,
);
}
}

View File

@ -1,34 +1,33 @@
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:vikunja_app/models/user.dart';
@JsonSerializable()
class TaskAttachment {
int id, taskId;
DateTime? created;
User? createdBy;
final int id, taskId;
late final DateTime created;
final User createdBy;
// TODO: add file
TaskAttachment({
required this.id,
this.id = -1,
required this.taskId,
this.created,
this.createdBy,
});
DateTime? created,
required this.createdBy,
}) {
this.created = created ?? DateTime.now();
}
TaskAttachment.fromJSON(Map<String, dynamic> json)
: id = json['id'],
taskId = json['task_id'],
created = DateTime.parse(json['created']),
createdBy = json['created_by'] == null
? null
: User.fromJson(json['created_by']);
createdBy = User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'id': id != -1 ? id : null,
'task_id': taskId,
'created': created?.toUtc().toIso8601String(),
'created_by': createdBy?.toJSON(),
'created': created.toUtc().toIso8601String(),
'created_by': createdBy.toJSON(),
};
}

View File

@ -3,18 +3,37 @@ import 'package:vikunja_app/global.dart';
class User {
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)
: id = json['id'],
email = json.containsKey('email') ? json['email'] : '',
username = json['username'];
name = json.containsKey('name') ? json['name'] : '',
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) {
return VikunjaGlobal.of(context).client.base! + "/avatar/${this.username}";
String avatarUrl(BuildContext context) {
return VikunjaGlobal.of(context).client.base + "/avatar/${this.username}";
}
}

View File

@ -43,7 +43,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
.asMap()
.forEach((i, namespace) => namespacesList.add(new ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace.title ?? ""),
title: new Text(namespace.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
@ -87,7 +87,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
@override
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
final currentUser = VikunjaGlobal.of(context).currentUser;
if (_selectedDrawerIndex != _previousDrawerIndex || drawerItem == null)
drawerItem = _getDrawerItemWidget(_selectedDrawerIndex);
@ -107,15 +107,15 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
))).whenComplete(() => _loadNamespaces()))
],
),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail: currentUser?.email == null
? null
: Text(currentUser?.email ?? ""),
accountName: currentUser?.username == null
? null
: Text(currentUser?.username ?? ""),
drawer: Drawer(
child: Column(children: <Widget>[
UserAccountsDrawerHeader(
accountName: currentUser != null
? Text(currentUser.username)
: null,
accountEmail: currentUser != null
? Text(currentUser.name)
: null,
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
@ -134,12 +134,12 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
Theme.of(context).primaryColor, BlendMode.multiply)),
),
),
new Builder(
Builder(
builder: (BuildContext context) => Expanded(
child: _showUserDetails
? _userDetailsWidget(context)
: _namespacesWidget())),
new Align(
Align(
alignment: FractionalOffset.bottomLeft,
child: Builder(
builder: (context) => ListTile(
@ -151,7 +151,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
),
),
),
new Align(
Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
builder: (context) => ListTile(
@ -197,9 +197,14 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
}
_addNamespace(String name, BuildContext context) {
final currentUser = VikunjaGlobal.of(context).currentUser;
if (currentUser == null) {
return;
}
VikunjaGlobal.of(context)
.namespaceService
.create(Namespace(id: 0, title: name))
.create(Namespace(title: name, owner: currentUser))
.then((_) {
_loadNamespaces();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(

View File

@ -103,20 +103,31 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
context: context,
builder: (_) =>
AddDialog(
onAddTask: (task) => _addTask(task, context),
onAddTask: (title, dueDate) => _addTask(title, dueDate, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
}
}
_addTask(Task task, BuildContext context) {
var globalState = VikunjaGlobal.of(context);
globalState.taskService.add(defaultList!, task).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'),
));
_loadList(context).then((value) => setState((){}));
});
Future<void> _addTask(
String title, DateTime? dueDate, BuildContext context) async {
final globalState = VikunjaGlobal.of(context);
if (globalState.currentUser == null) {
return;
}
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(() {}));
}

View File

@ -50,7 +50,7 @@ class _ListPageState extends State<ListPage> {
List<Task> _loadingTasks = [];
int _currentPage = 1;
bool _loading = true;
bool? displayDoneTasks;
bool displayDoneTasks = false;
ListProvider? taskState;
PageController? _pageController;
Map<int, BucketProps> _bucketProps = {};
@ -159,7 +159,7 @@ class _ListPageState extends State<ListPage> {
ListView _listView(BuildContext context) {
return ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0),
itemCount: taskState!.tasks.length,
itemCount: taskState!.tasks.length * 2,
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
@ -170,11 +170,6 @@ class _ListPageState extends State<ListPage> {
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 &&
index == taskState!.tasks.length)
throw Exception("Check itemCount attribute");
@ -341,7 +336,7 @@ class _ListPageState extends State<ListPage> {
});
});
if (_bucketProps[bucket.id]!.titleController.text.isEmpty)
_bucketProps[bucket.id]!.titleController.text = bucket.title ?? "";
_bucketProps[bucket.id]!.titleController.text = bucket.title;
return Stack(
children: <Widget>[
@ -389,7 +384,7 @@ class _ListPageState extends State<ListPage> {
padding: const EdgeInsets.only(right: 2),
child: Text(
'${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
? Colors.red : null,
),
@ -426,7 +421,7 @@ class _ListPageState extends State<ListPage> {
}
},
itemBuilder: (context) {
final bool enableDelete = (taskState?.buckets.length ?? 0) > 1;
final bool enableDelete = taskState!.buckets.length > 1;
return <PopupMenuEntry<BucketMenu>>[
PopupMenuItem<BucketMenu>(
value: BucketMenu.limit,
@ -613,7 +608,7 @@ class _ListPageState extends State<ListPage> {
context: context,
listId: _list!.id,
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]) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null,
Future<void> _addItem(String title, BuildContext context, [Bucket? bucket]) async {
final currentUser = VikunjaGlobal.of(context).currentUser;
if (currentUser == null) {
return;
}
final newTask = Task(
title: title,
createdBy: globalState.currentUser,
createdBy: currentUser,
done: false,
bucketId: bucket?.id,
identifier: '',
listId: _list!.id,
);
setState(() => _loadingTasks.add(newTask));
return Provider.of<ListProvider>(context, listen: false)
@ -679,23 +677,27 @@ class _ListPageState extends State<ListPage> {
);
}
Future<void> _addBucket(String title, BuildContext context) {
return Provider.of<ListProvider>(context, listen: false).addBucket(
Future<void> _addBucket(String title, BuildContext context) async {
final currentUser = VikunjaGlobal.of(context).currentUser;
if (currentUser == null) {
return;
}
await Provider.of<ListProvider>(context, listen: false).addBucket(
context: context,
newBucket: Bucket(
id: 0,
title: title,
createdBy: VikunjaGlobal.of(context).currentUser,
createdBy: currentUser,
listId: _list!.id,
limit: 0,
),
listId: _list!.id,
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The bucket was added successfully!'),
));
setState(() {});
});
);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The bucket was added successfully!'),
));
setState(() {});
}
Future<void> _updateBucket(BuildContext context, Bucket bucket) {
@ -715,16 +717,17 @@ class _ListPageState extends State<ListPage> {
context: context,
listId: bucket.listId,
bucketId: bucket.id,
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Row(
children: <Widget>[
Text('${bucket.title} was deleted.'),
Icon(Icons.delete),
],
),
));
});
);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Row(
children: <Widget>[
Text('${bucket.title} was deleted.'),
Icon(Icons.delete),
],
),
));
_onViewTapped(1);
}
}

View File

@ -18,9 +18,9 @@ class ListEditPage extends StatefulWidget {
class _ListEditPageState extends State<ListEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String? _title, _description;
String _title = '', _description = '';
bool? displayDoneTasks;
int listId = -1;
late int listId;
@override
void initState(){
@ -52,7 +52,7 @@ class _ListEditPageState extends State<ListEditPage> {
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.list.title,
onSaved: (title) => _title = title,
onSaved: (title) => _title = title ?? '',
validator: (title) {
//if (title?.length < 3 || title.length > 250) {
// return 'The title needs to have between 3 and 250 characters.';
@ -71,7 +71,7 @@ class _ListEditPageState extends State<ListEditPage> {
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.list.description,
onSaved: (description) => _description = description,
onSaved: (description) => _description = description ?? '',
validator: (description) {
if(description == null)
return null;
@ -88,29 +88,16 @@ class _ListEditPageState extends State<ListEditPage> {
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: displayDoneTasks != null ?
CheckboxListTile(
value: displayDoneTasks,
title: Text("Show done tasks"),
onChanged: (value) {
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value == false ? "0" : "1");
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"),
),),
child: CheckboxListTile(
value: displayDoneTasks ?? false,
title: Text("Show done tasks"),
onChanged: (value) {
value ??= false;
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value ? "1" : "0");
setState(() => displayDoneTasks = value);
},
),
),
Builder(
builder: (context) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),

View File

@ -32,41 +32,40 @@ class _TaskEditPageState extends State<TaskEditPage> {
int? _priority;
DateTime? _dueDate, _startDate, _endDate;
List<DateTime?>? _reminderDates;
late final List<DateTime> _reminderDates;
String? _title, _description, _repeatAfterType;
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.
List<Label>? _suggestedLabels;
var _reminderInputs = <Widget>[];
final _reminderInputs = <Widget>[];
final _labelTypeAheadController = TextEditingController();
Color? _color;
Color? _pickerColor;
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
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(
onWillPop: () {
if(_changed) {
@ -221,15 +220,15 @@ class _TaskEditPageState extends State<TaskEditPage> {
),
onTap: () {
// We add a new entry every time we add a new input, to make sure all inputs have a place where they can put their value.
_reminderDates!.add(null);
var currentIndex = _reminderDates!.length - 1;
_reminderDates.add(DateTime(0));
var currentIndex = _reminderDates.length - 1;
// FIXME: Why does putting this into a row fails?
setState(() => _reminderInputs.add(
VikunjaDateTimePicker(
label: 'Reminder',
onSaved: (reminder) =>
_reminderDates![currentIndex] = reminder,
_reminderDates[currentIndex] = reminder ?? DateTime(0),
onChanged: (_) => _changed = true,
initialValue: DateTime.now(),
),
@ -262,7 +261,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
),
Wrap(
spacing: 10,
children: _labels!.map((Label label) {
children: _labels.map((Label label) {
return LabelComponent(
label: label,
onDelete: () {
@ -312,9 +311,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
ElevatedButton(
child: Text(
'Color',
style: _resetColor ? null : TextStyle(
color: (_color ?? widget.task.color)
.computeLuminance() > 0.5 ? Colors.black : Colors.white,
style: (_resetColor || (_color ?? widget.task.color) == null) ? null : TextStyle(
color: (_color ?? widget.task.color)!.computeLuminance() > 0.5 ? Colors.black : Colors.white,
),
),
style: _resetColor ? null : ButtonStyle(
@ -364,7 +362,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
setState(() => _loading = true);
// Removes all reminders with no value set.
_reminderDates?.removeWhere((d) => d == null);
_reminderDates.removeWhere((d) => d == DateTime(0));
Task updatedTask = widget.task.copyWith(
title: _title,
@ -417,7 +415,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
_removeLabel(Label label) {
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)
.labelService.getAll(query: query).then((labels) {
// 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;
List<String?> labelText = labels.map((label) => label.title).toList();
return labelText;
@ -437,7 +435,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
bool found = false;
_suggestedLabels?.forEach((label) {
if (label.title == labelTitle) {
_labels?.add(label);
_labels.add(label);
found = true;
}
});
@ -448,19 +446,27 @@ class _TaskEditPageState extends State<TaskEditPage> {
setState(() {});
}
_createAndAddLabel(String labelTitle) {
void _createAndAddLabel(String labelTitle) {
// Only add a label if there are none to add
if (labelTitle.isEmpty || (_suggestedLabels?.isNotEmpty ?? false)) {
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)
.labelService
.create(newLabel)
.then((createdLabel) {
setState(() {
_labels?.add(createdLabel);
_labels.add(createdLabel);
_labelTypeAheadController.clear();
});
});
@ -505,7 +511,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
}
_onColorEdit() {
_pickerColor = _resetColor
_pickerColor = _resetColor || (_color ?? widget.task.color) == null
? Colors.black
: _color ?? widget.task.color;
showDialog(
@ -524,11 +530,11 @@ class _TaskEditPageState extends State<TaskEditPage> {
),
actions: <TextButton>[
TextButton(
child: Text('Cancel'),
child: Text('CANCEL'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: Text('Reset'),
child: Text('RESET'),
onPressed: () {
setState(() {
_color = null;
@ -539,7 +545,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
},
),
TextButton(
child: Text('Ok'),
child: Text('OK'),
onPressed: () {
if (_pickerColor != Colors.black) setState(() {
_color = _pickerColor;

View File

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart';
import 'package:provider/provider.dart';
@ -48,7 +47,7 @@ class _NamespacePageState extends State<NamespacePage>
key: Key(ls.id.toString()),
direction: DismissDirection.startToEnd,
child: ListTile(
title: new Text(ls.title ?? ""),
title: new Text(ls.title),
onTap: () => _openList(context, ls),
trailing: Icon(Icons.arrow_right),
),
@ -84,7 +83,7 @@ class _NamespacePageState extends State<NamespacePage>
_loadLists();
}
Future _removeList(TaskList list) {
Future<void> _removeList(TaskList list) {
return VikunjaGlobal.of(context)
.listService
.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)
.listService
.create(
widget.namespace.id,
TaskList(
id: 0,
title: name,
tasks: [],
namespaceId: widget.namespace.id))
title: name,
tasks: [],
namespaceId: widget.namespace.id,
owner: curentUser,
))
.then((_) {
setState(() {});
_loadLists();

View File

@ -16,7 +16,14 @@ class NamespaceEditPage extends StatefulWidget {
class _NamespaceEditPageState extends State<NamespaceEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String? _name, _description;
late String _name, _description;
@override
void initState() {
_name = widget.namespace.title;
_description = widget.namespace.description;
super.initState();
}
@override
Widget build(BuildContext ctx) {
@ -37,7 +44,7 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.namespace.title,
onSaved: (name) => _name = name,
onSaved: (name) => _name = name ?? '',
validator: (name) {
//if (name.length < 3 || name.length > 250) {
// return 'The name needs to have between 3 and 250 characters.';
@ -56,7 +63,7 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.namespace.description,
onSaved: (description) => _description = description,
onSaved: (description) => _description = description ?? '',
validator: (description) {
//if (description.length > 1000) {
// return 'The description can have a maximum of 1000 characters.';
@ -80,7 +87,7 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
_saveNamespace(context);
}
}
: () => null,
: null,
child: _loading
? CircularProgressIndicator()
: 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?
// aka updating the existing namespace we got from context (setters?)
Namespace updatedNamespace = Namespace(
id: widget.namespace.id,
title: _name,
description: _description,
owner: widget.namespace.owner);
id: widget.namespace.id,
title: _name,
description: _description,
owner: widget.namespace.owner,
created: widget.namespace.created,
updated: widget.namespace.updated,
);
VikunjaGlobal.of(context)
.namespaceService

View File

@ -32,7 +32,7 @@ class SettingsPageState extends State<SettingsPage> {
ListTile(
title: Text("Default List"),
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,
onChanged: (int? value){
setState(() => defaultList = value);

View File

@ -33,28 +33,28 @@ class _LoginPageState extends State<LoginPage> {
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () {
if(VikunjaGlobal.of(context).expired) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
"Login has expired. Please reenter your details!")));
setState(() {
_serverController.text = VikunjaGlobal.of(context).client.base ?? "";
_usernameController.text = VikunjaGlobal.of(context).currentUser?.username ?? "";
});
}
});
Future.delayed(Duration.zero, () {
if(VikunjaGlobal.of(context).expired) {
ScaffoldMessenger.of(context)
.showSnackBar(
SnackBar(
content: Text(
"Login has expired. Please reenter your details!")));
setState(() {
_serverController.text = VikunjaGlobal.of(context).client.base;
_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
Widget build(BuildContext ctx) {
Client client = VikunjaGlobal.of(context).client;
if(client.ignoreCertificates == null)
VikunjaGlobal.of(context).settingsManager.getIgnoreCertificates().then((value) => setState(() => client.ignoreCertificates = value == "1" ? true:false));
Client client = VikunjaGlobal.of(context).client;
return Scaffold(
body: Center(
@ -131,7 +131,7 @@ class _LoginPageState extends State<LoginPage> {
_loginUser(context);
}
}
: () => null,
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Login'),

View File

@ -8,7 +8,7 @@ import 'package:vikunja_app/models/user.dart';
import 'package:vikunja_app/service/services.dart';
// Data for mocked services
var _users = {1: User(1, 'test@testuser.org', 'test1')};
var _users = {1: User(id: 1, username: 'test1')};
var _namespaces = {
1: Namespace(
@ -17,7 +17,7 @@ var _namespaces = {
created: DateTime.now(),
updated: DateTime.now(),
description: 'A namespace for testing purposes',
owner: _users[1],
owner: _users[1]!,
)
};
@ -30,7 +30,7 @@ var _lists = {
id: 1,
title: 'List 1',
tasks: _tasks.values.toList(),
owner: _users[1],
owner: _users[1]!,
description: 'A nice list',
created: DateTime.now(),
updated: DateTime.now(),
@ -41,12 +41,12 @@ var _tasks = {
1: Task(
id: 1,
title: 'Task 1',
createdBy: _users[1],
createdBy: _users[1]!,
updated: DateTime.now(),
created: DateTime.now(),
description: 'A descriptive task',
done: false,
identifier: '',
listId: 1,
)
};
@ -146,7 +146,7 @@ class MockedTaskService implements TaskService {
@override
Future delete(int taskId) {
_lists.forEach(
(_, list) => list.tasks.removeWhere((task) => task?.id == taskId));
(_, list) => list.tasks.removeWhere((task) => task.id == taskId));
_tasks.remove(taskId);
return Future.value();
}
@ -154,12 +154,12 @@ class MockedTaskService implements TaskService {
@override
Future<Task> update(Task task) {
_lists.forEach((_, list) {
if (list.tasks.where((t) => t?.id == task.id).length > 0) {
list.tasks.removeWhere((t) => t?.id == task.id);
if (list.tasks.where((t) => t.id == task.id).length > 0) {
list.tasks.removeWhere((t) => t.id == task.id);
list.tasks.add(task);
}
});
return Future.value(_tasks[task.id ?? 0] = task);
return Future.value(_tasks[task.id] = task);
}
@override

View File

@ -87,14 +87,17 @@ class ListProvider with ChangeNotifier {
}
Future<void> addTaskByTitle(
{required BuildContext context, required String title, required int listId}) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: 0,
identifier: '',
{required BuildContext context, required String title, required int listId}) async{
final globalState = VikunjaGlobal.of(context);
if (globalState.currentUser == null) {
return;
}
final newTask = Task(
title: title,
createdBy: globalState.currentUser,
createdBy: globalState.currentUser!,
done: false,
listId: listId,
);
_isLoading = true;
notifyListeners();
@ -116,11 +119,7 @@ class ListProvider with ChangeNotifier {
_tasks.insert(0, task);
if (_buckets.isNotEmpty) {
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
if (bucket.tasks != null) {
bucket.tasks.add(task);
} else {
bucket.tasks = <Task>[task];
}
bucket.tasks.add(task);
}
_isLoading = false;
notifyListeners();
@ -159,7 +158,7 @@ class ListProvider with ChangeNotifier {
return VikunjaGlobal.of(context).bucketService.update(bucket)
.then((rBucket) {
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
_buckets.sort((a, b) => a.position.compareTo(b.position));
_buckets.sort((a, b) => a.position!.compareTo(b.position!));
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 newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId);
if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)) index--;
@ -212,7 +211,7 @@ class ListProvider with ChangeNotifier {
if (_tasks.isNotEmpty) {
_tasks[_tasks.indexWhere((t) => t.id == task.id)] = task;
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;

View File

@ -1,5 +1,3 @@
import 'package:meta/meta.dart';
class CheckboxStatistics {
final int total;
final int checked;
@ -28,7 +26,7 @@ MatchedCheckboxes getCheckboxesInText(String text) {
final matches = RegExp(r'[*-] \[[ x]]').allMatches(text);
for (final match in matches) {
if (match[0]!.endsWith(checkedString))
if (match[0]?.endsWith(checkedString) ?? false)
checked.add(match);
else
unchecked.add(match);

View File

@ -1,13 +1,13 @@
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,}))$');
bool isEmail(email) {
return _emailRegex.hasMatch(email);
bool isEmail(String? email) {
return _emailRegex.hasMatch(email ?? '');
}
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]+)?');
bool isUrl(url) {
return _url.hasMatch(url);
bool isUrl(String? url) {
return _url.hasMatch(url ?? '');
}

View File

@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:test/test.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/user.dart';
void main() {
test('label color from json', () {
@ -14,9 +15,9 @@ void main() {
});
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();
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}');
});
}