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", "name": "Flutter",
"request": "launch", "request": "launch",
"type": "dart", "type": "dart",
"flutterMode": "debug" "flutterMode": "debug",
"args": [
"--flavor",
"main"
],
} }
] ]
} }

View File

@ -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);
} }

View File

@ -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)));
} }

View File

@ -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);
}, },

View File

@ -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,

View File

@ -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!'),
))); ));
} }
} }

View File

@ -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>(

View File

@ -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,
), ),
), ),

View File

@ -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;

View File

@ -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(),
}; };
} }

View File

@ -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;
} }

View File

@ -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() => {

View File

@ -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
}; };
} }
} }

View File

@ -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
}; };
} }

View File

@ -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,
); );
} }
} }

View File

@ -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(),
}; };
} }

View File

@ -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}";
} }
} }

View File

@ -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(

View File

@ -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(() {}));
} }

View File

@ -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);
} }
} }

View File

@ -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),

View File

@ -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;

View File

@ -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();

View File

@ -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

View File

@ -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);

View File

@ -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'),

View File

@ -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

View File

@ -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;

View File

@ -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);

View File

@ -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 ?? '');
} }

View File

@ -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}');
}); });
} }