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

first step of migration to null-safety

This commit is contained in:
Benimautner 2022-08-27 23:04:43 +02:00
parent 48067d6b34
commit b5266020a8
48 changed files with 501 additions and 479 deletions

View File

@ -36,10 +36,10 @@ class Client {
configure(token: token, base: base, authenticated: authenticated);
}
void reload_ignore_certs(bool val) {
ignoreCertificates = val;
void reload_ignore_certs(bool? val) {
ignoreCertificates = val ?? false;
HttpOverrides.global = new IgnoreCertHttpOverrides(ignoreCertificates);
VikunjaGlobal.of(global.currentContext!).settingsManager.setIgnoreCertificates(val);
VikunjaGlobal.of(global.currentContext!).settingsManager.setIgnoreCertificates(ignoreCertificates);
}
@ -53,9 +53,12 @@ class Client {
int get hashCode => _token.hashCode;
void configure({String? token, String? base, bool? authenticated}) {
_token = token!;
_base = base!.endsWith('/api/v1') ? base : '$base/api/v1';
this.authenticated = authenticated!;
if(token != null)
_token = token;
if(base != null)
_base = base.endsWith('/api/v1') ? base : '$base/api/v1';
if(authenticated != null)
this.authenticated = authenticated;
}
@ -86,7 +89,7 @@ class Client {
Future<Response> delete(String url) {
return http
.delete(
'${this.base}$url'.toUri(),
'${this.base}$url'.toUri()!,
headers: _headers,
)
.then(_handleResponse, onError: _handleError);
@ -96,7 +99,7 @@ class Client {
log('post');
return http
.post(
'${this.base}$url'.toUri(),
'${this.base}$url'.toUri()!,
headers: _headers,
body: _encoder.convert(body),
)
@ -106,7 +109,7 @@ class Client {
Future<Response> put(String url, {dynamic body}) {
return http
.put(
'${this.base}$url'.toUri(),
'${this.base}$url'.toUri()!,
headers: _headers,
body: _encoder.convert(body),
)

View File

@ -10,14 +10,14 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
@override
Future<Label> create(LabelTask lt) async {
return client
.put('/tasks/${lt.task.id}/labels', body: lt.toJSON())
.put('/tasks/${lt.task!.id}/labels', body: lt.toJSON())
.then((result) => Label.fromJson(result.body));
}
@override
Future<Label> delete(LabelTask lt) async {
return client
.delete('/tasks/${lt.task.id}/labels/${lt.label.id}')
.delete('/tasks/${lt.task!.id}/labels/${lt.label.id}')
.then((result) => Label.fromJson(result.body));
}
@ -26,7 +26,7 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
String? params =
query == null ? null : '?s=' + Uri.encodeQueryComponent(query);
return client.get('/tasks/${lt.task.id}/labels$params').then(
return client.get('/tasks/${lt.task!.id}/labels$params').then(
(label) => convertList(label, (result) => Label.fromJson(result)));
}
}

View File

@ -10,7 +10,9 @@ class LabelTaskBulkAPIService extends APIService
LabelTaskBulkAPIService(Client client) : super(client);
@override
Future<List<Label>> update(Task task, List<Label> labels) {
Future<List<Label>> update(Task task, List<Label>? labels) {
if(labels == null)
labels = [];
return client
.post('/tasks/${task.id}/labels/bulk',
body: LabelTaskBulk(labels: labels).toJSON())

View File

@ -88,7 +88,7 @@ class ListAPIService extends APIService implements ListService {
}
@override
void setDefaultList(int listId) {
void setDefaultList(int? listId) {
_storage.write(key: "default_list_id", value: listId.toString());
}
}

View File

@ -65,11 +65,11 @@ 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: null,
widget.onAddTask!(Task(id: 0,
title: textController.text,
done: false,
createdBy: null,
dueDate: customDueDate));
dueDate: customDueDate, identifier: ''));
}
Navigator.pop(context);
},
@ -82,7 +82,7 @@ class AddDialogState extends State<AddDialog> {
return Row(children: [
Checkbox(value: newTaskDue == thisNewTaskDue, onChanged: (value) {
newTaskDue = thisNewTaskDue;
setState(() => customDueDate = DateTime.now().add(newTaskDueToDuration[thisNewTaskDue]));}, shape: CircleBorder(),),
setState(() => customDueDate = DateTime.now().add(newTaskDueToDuration[thisNewTaskDue]!));}, shape: CircleBorder(),),
Text(name),
]);
}

View File

@ -5,7 +5,7 @@ import 'package:vikunja_app/models/bucket.dart';
class BucketLimitDialog extends StatefulWidget {
final Bucket bucket;
const BucketLimitDialog({Key key, @required this.bucket}) : super(key: key);
const BucketLimitDialog({Key? key, required this.bucket}) : super(key: key);
@override
State<BucketLimitDialog> createState() => _BucketLimitDialogState();

View File

@ -25,11 +25,11 @@ class BucketTaskCard extends StatefulWidget {
final void Function(Task, int) onAccept;
const BucketTaskCard({
Key key,
@required this.task,
@required this.index,
@required this.onDragUpdate,
@required this.onAccept,
Key? key,
required this.task,
required this.index,
required this.onDragUpdate,
required this.onAccept,
}) : assert(task != null),
assert(index != null),
assert(onDragUpdate != null),
@ -41,10 +41,10 @@ class BucketTaskCard extends StatefulWidget {
}
class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAliveClientMixin {
Size _cardSize;
Size? _cardSize;
bool _dragging = false;
DropLocation _dropLocation = DropLocation.none;
TaskData _dropData;
TaskData? _dropData;
@override
Widget build(BuildContext context) {
@ -63,20 +63,20 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
Text(
widget.task.identifier.isNotEmpty
? '#${widget.task.identifier.substring(1)}' : '${widget.task.id}',
style: theme.textTheme.subtitle2.copyWith(
style: theme.textTheme.subtitle2?.copyWith(
color: Colors.grey,
),
),
],
);
if (widget.task.done) {
if (widget.task.done ?? false) {
identifierRow.children.insert(0, Container(
constraints: chipConstraints,
padding: EdgeInsets.only(right: 4),
child: FittedBox(
child: Chip(
label: Text('Done'),
labelStyle: theme.textTheme.labelLarge.copyWith(
labelStyle: theme.textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.black : Colors.white,
@ -91,8 +91,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?.copyWith(
color: widget.task.textColor,
),
),
@ -100,7 +100,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
],
);
if (widget.task.hasDueDate) {
final duration = widget.task.dueDate.difference(DateTime.now());
final duration = widget.task.dueDate!.difference(DateTime.now());
final pastDue = duration.isNegative && !widget.task.done;
titleRow.children.add(Container(
constraints: chipConstraints,
@ -112,7 +112,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?.copyWith(
color: pastDue ? Colors.red : null,
),
backgroundColor: pastDue ? Colors.red.withAlpha(20) : null,
@ -126,19 +126,19 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
spacing: 4,
runSpacing: 4,
);
widget.task.labels?.sort((a, b) => a.title.compareTo(b.title));
widget.task.labels?.asMap()?.forEach((i, label) {
widget.task.labels?.sort((a, b) => a.title?.compareTo(b.title ?? "") ?? 0);
widget.task.labels?.asMap().forEach((i, label) {
labelRow.children.add(Chip(
label: Text(label.title),
labelStyle: theme.textTheme.labelLarge.copyWith(
label: Text(label.title ?? ""),
labelStyle: theme.textTheme.labelLarge?.copyWith(
color: label.textColor,
),
backgroundColor: label.color,
));
});
if (widget.task.hasCheckboxes) {
final checkboxStatistics = widget.task.checkboxStatistics;
final iconSize = (theme.textTheme.labelLarge.fontSize ?? 14) + 2;
final checkboxStatistics = widget.task.checkboxStatistics!;
final iconSize = (theme.textTheme.labelLarge?.fontSize ?? 14) + 2;
labelRow.children.add(Chip(
avatar: Container(
constraints: BoxConstraints(maxHeight: iconSize, maxWidth: iconSize),
@ -153,7 +153,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
),
));
}
if (widget.task.attachments != null && widget.task.attachments.isNotEmpty) {
if (widget.task.attachments != null && widget.task.attachments!.isNotEmpty) {
labelRow.children.add(Chip(
label: Transform.rotate(
angle: -pi / 4.0,
@ -161,7 +161,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
),
));
}
if (widget.task.description.isNotEmpty) {
if (widget.task.description != null && widget.task.description!.isNotEmpty) {
labelRow.children.add(Chip(
label: Icon(Icons.notes),
));
@ -214,7 +214,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
);
return LongPressDraggable<TaskData>(
data: TaskData(widget.task, _cardSize),
data: TaskData(widget.task, _cardSize!),
maxSimultaneousDrags: taskState.taskDragging ? 0 : 1, // only one task can be dragged at a time
onDragStarted: () {
taskState.taskDragging = true;
@ -268,8 +268,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,18 +282,18 @@ 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),
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.above),
onAccept: dragTargetOnAccept,
onLeave: dragTargetOnLeave,
builder: (_, __, ___) => SizedBox.expand(),
),
),
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),
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.below),
onAccept: dragTargetOnAccept,
onLeave: dragTargetOnLeave,
builder: (_, __, ___) => SizedBox.expand(),

View File

@ -10,12 +10,10 @@ class SliverBucketList extends StatelessWidget {
final DragUpdateCallback onTaskDragUpdate;
const SliverBucketList({
Key key,
@required this.bucket,
@required this.onTaskDragUpdate,
}) : assert(bucket != null),
assert(onTaskDragUpdate != null),
super(key: key);
Key? key,
required this.bucket,
required this.onTaskDragUpdate,
}) : super(key: key);
@override
Widget build(BuildContext context) {

View File

@ -6,8 +6,8 @@ class SliverBucketPersistentHeader extends StatelessWidget {
final double maxExtent;
const SliverBucketPersistentHeader({
Key key,
@required this.child,
Key? key,
required this.child,
this.minExtent = 10.0,
this.maxExtent = 10.0,
}) : super(key: key);

View File

@ -12,12 +12,11 @@ class TaskTile extends StatefulWidget {
final Function onEdit;
final bool showInfo;
final bool loading;
final ValueSetter<bool> onMarkedAsDone;
final ValueSetter<bool>? onMarkedAsDone;
const TaskTile(
{Key key, @required this.task, this.onEdit, this.loading = false, this.showInfo = false, this.onMarkedAsDone})
: assert(task != null),
super(key: key);
{Key? key, required this.task, required this.onEdit, this.loading = false, this.showInfo = false, this.onMarkedAsDone})
: super(key: key);
/*
@override
TaskTileState createState() {
@ -32,13 +31,12 @@ class TaskTile extends StatefulWidget {
class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
Task _currentTask;
TaskTileState(this._currentTask)
: assert(_currentTask != null);
TaskTileState(this._currentTask);
@override
Widget build(BuildContext context) {
super.build(context);
Duration durationUntilDue = _currentTask.dueDate.difference(DateTime.now());
Duration? durationUntilDue = _currentTask.dueDate?.difference(DateTime.now());
if (_currentTask.loading) {
return ListTile(
leading: Padding(
@ -50,11 +48,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 == null || _currentTask.description!.isEmpty
? null
: Text(_currentTask.description),
: Text(_currentTask.description ?? ""),
trailing: IconButton(
icon: Icon(Icons.settings), onPressed: () { },
),
@ -74,14 +72,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 ?? false,
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: TextStyle(color: durationUntilDue.isNegative ? Colors.red : null),)
: _currentTask.description == null || _currentTask.description!.isEmpty
? null
: Text(_currentTask.description),
: Text(_currentTask.description ?? ""),
secondary:
IconButton(icon: Icon(Icons.settings), onPressed: () {
Navigator.push<Task>(
@ -100,7 +98,8 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
);
}
void _change(bool value) async {
void _change(bool? value) async {
value = value ?? false;
setState(() {
this._currentTask.loading = true;
});

View File

@ -5,16 +5,16 @@ import 'package:vikunja_app/theme/constants.dart';
class VikunjaDateTimePicker extends StatelessWidget {
final String label;
final Function onSaved;
final Function onChanged;
final DateTime initialValue;
final void Function(DateTime?)? onSaved;
final void Function(DateTime?)? onChanged;
final DateTime? initialValue;
final EdgeInsetsGeometry padding;
final Icon icon;
final InputBorder border;
const VikunjaDateTimePicker({
Key key,
@required this.label,
Key? key,
required this.label,
this.onSaved,
this.onChanged,
this.initialValue,
@ -28,9 +28,9 @@ class VikunjaDateTimePicker extends StatelessWidget {
return DateTimeField(
//dateOnly: false,
//editable: false, // Otherwise editing the date is not possible, this setting affects the underlying text field.
initialValue: initialValue == null || initialValue.year <= 1
initialValue: initialValue == null || initialValue!.year <= 1
? null
: initialValue.toLocal(),
: initialValue!.toLocal(),
format: vDateFormatLong,
decoration: InputDecoration(
labelText: label,
@ -47,7 +47,7 @@ class VikunjaDateTimePicker extends StatelessWidget {
);
}
Future<DateTime> _showDatePickerFuture(context, currentValue) {
Future<DateTime?> _showDatePickerFuture(context, currentValue) {
return showDialog(
context: context,
builder: (_) => DatePickerDialog(

View File

@ -6,7 +6,7 @@ class LabelComponent extends StatefulWidget {
final Label label;
final VoidCallback onDelete;
const LabelComponent({Key key, @required this.label, this.onDelete})
const LabelComponent({Key? key, required this.label, required this.onDelete})
: super(key: key);
@override
@ -24,7 +24,7 @@ class LabelComponentState extends State<LabelComponent> {
return Chip(
label: Text(
widget.label.title,
widget.label.title ?? "",
style: TextStyle(
color: textColor,
),

View File

@ -1,3 +1,3 @@
extension StringExtensions on String {
Uri toUri() => Uri.tryParse(this);
Uri? toUri() => Uri.tryParse(this);
}

View File

@ -27,7 +27,7 @@ class VikunjaGlobal extends StatefulWidget {
final Widget child;
final Widget login;
VikunjaGlobal({this.child, this.login});
VikunjaGlobal({required this.child, required this.login});
@override
VikunjaGlobalState createState() => VikunjaGlobalState();
@ -35,21 +35,21 @@ class VikunjaGlobal extends StatefulWidget {
static VikunjaGlobalState of(BuildContext context) {
var widget =
context.dependOnInheritedWidgetOfExactType<_VikunjaGlobalInherited>();
return widget.data;
return widget!.data;
}
}
class VikunjaGlobalState extends State<VikunjaGlobal> {
final FlutterSecureStorage _storage = new FlutterSecureStorage();
User _currentUser;
User? _currentUser;
bool _loading = true;
bool expired = false;
Client _client;
UserService _newUserService;
late Client _client;
UserService? _newUserService;
User get currentUser => _currentUser;
User? get currentUser => _currentUser;
Client get client => _client;
@ -58,7 +58,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
UserManager get userManager => new UserManager(_storage);
UserService get newUserService => _newUserService;
UserService? get newUserService => _newUserService;
ServerService get serverService => new ServerAPIService(client);
@ -78,7 +78,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
NotificationClass get notifications => new NotificationClass();
notifs.NotificationAppLaunchDetails notifLaunch;
notifs.NotificationAppLaunchDetails? notifLaunch;
LabelService get labelService => new LabelAPIService(client);
@ -101,11 +101,11 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
icon: 'ic_launcher_foreground',
importance: notifs.Importance.high
);
notifs.IOSNotificationDetails iOSSpecifics;
notifs.NotificationDetails platformChannelSpecificsDueDate;
notifs.NotificationDetails platformChannelSpecificsReminders;
late notifs.IOSNotificationDetails iOSSpecifics;
late notifs.NotificationDetails platformChannelSpecificsDueDate;
late notifs.NotificationDetails platformChannelSpecificsReminders;
String currentTimeZone;
late String currentTimeZone;
@ -126,7 +126,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
}
void changeUser(User newUser, {String token, String base}) async {
void changeUser(User newUser, {String? token, String? base}) async {
setState(() {
_loading = true;
});
@ -163,20 +163,20 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
taskService.getAll().then((value) =>
value.forEach((task) {
if(task.reminderDates != null)
task.reminderDates.forEach((reminder) {
scheduleNotification("Reminder", "This is your reminder for '" + task.title + "'",
task.reminderDates!.forEach((reminder) {
scheduleNotification("Reminder", "This is your reminder for '" + task.title! + "'",
notificationsPlugin,
reminder,
reminder!,
currentTimeZone,
platformChannelSpecifics: platformChannelSpecificsReminders,
platformChannelSpecificsReminders,
id: (reminder.millisecondsSinceEpoch/1000).floor());
});
if(task.dueDate != null)
scheduleNotification("Due Reminder","The task '" + task.title + "' is due.",
scheduleNotification("Due Reminder","The task '" + task.title! + "' is due.",
notificationsPlugin,
task.dueDate,
task.dueDate!,
currentTimeZone,
platformChannelSpecifics: platformChannelSpecificsDueDate,
platformChannelSpecificsDueDate,
id: task.id);
})
);
@ -233,9 +233,9 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
});
return;
}
loadedCurrentUser = User(int.tryParse(currentUser), "", "");
loadedCurrentUser = User(int.tryParse(currentUser)!, "", "");
} catch (otherExceptions) {
loadedCurrentUser = User(int.tryParse(currentUser), "", "");
loadedCurrentUser = User(int.tryParse(currentUser)!, "", "");
}
setState(() {
_currentUser = loadedCurrentUser;
@ -248,12 +248,13 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
if (_loading) {
return new Center(child: new CircularProgressIndicator());
}
if(client != null && client.authenticated) {
if(client.authenticated) {
scheduleDueNotifications();
}
return new _VikunjaGlobalInherited(
data: this,
child: client == null || !client.authenticated ? widget.login : widget.child,
key: UniqueKey(),
child: !client.authenticated ? widget.login : widget.child,
);
}
}
@ -261,13 +262,13 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
class _VikunjaGlobalInherited extends InheritedWidget {
final VikunjaGlobalState data;
_VikunjaGlobalInherited({Key key, this.data, Widget child})
_VikunjaGlobalInherited({Key? key, required this.data, required Widget child})
: super(key: key, child: child);
@override
bool updateShouldNotify(_VikunjaGlobalInherited oldWidget) {
return (data.currentUser != null &&
data.currentUser.id != oldWidget.data.currentUser.id) ||
data.currentUser!.id != oldWidget.data.currentUser!.id) ||
data.client != oldWidget.data.client;
}
}

View File

@ -8,10 +8,10 @@ import 'package:vikunja_app/theme/theme.dart';
import 'package:http/http.dart';
class IgnoreCertHttpOverrides extends HttpOverrides {
bool ignoreCerts;
bool ignoreCerts = false;
IgnoreCertHttpOverrides(bool _ignore) {ignoreCerts = _ignore;}
@override
HttpClient createHttpClient(SecurityContext context) {
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
..badCertificateCallback = (_, __, ___) => ignoreCerts;
}
@ -19,15 +19,15 @@ class IgnoreCertHttpOverrides extends HttpOverrides {
void main() {
runApp(VikunjaGlobal(
child: new VikunjaApp(home: HomePage()),
login: new VikunjaApp(home: LoginPage())));
child: new VikunjaApp(home: HomePage(), key: UniqueKey(),),
login: new VikunjaApp(home: LoginPage(), key: UniqueKey(),)));
}
class VikunjaApp extends StatelessWidget {
final Widget home;
const VikunjaApp({Key key, this.home}) : super(key: key);
const VikunjaApp({Key? key, required this.home}) : super(key: key);
@override
Widget build(BuildContext context) {

View File

@ -13,10 +13,10 @@ import 'package:rxdart/subjects.dart' as rxSub;
import 'package:vikunja_app/global.dart';
class NotificationClass{
final int id;
final String title;
final String body;
final String payload;
final int? id;
final String? title;
final String? body;
final String? payload;
NotificationClass({this.id, this.body, this.payload, this.title});
final rxSub.BehaviorSubject<NotificationClass> didReceiveLocalNotificationSubject =
@ -32,18 +32,18 @@ class NotificationClass{
requestBadgePermission: false,
requestSoundPermission: false,
onDidReceiveLocalNotification:
(int id, String title, String body, String payload) async {
(int? id, String? title, String? body, String? payload) async {
didReceiveLocalNotificationSubject
.add(NotificationClass(id: id, title: title, body: body, payload: payload));
});
var initializationSettings = notifs.InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await notifsPlugin.initialize(initializationSettings,
onSelectNotification: (String payload) async {
onSelectNotification: (String? payload) async {
if (payload != null) {
print('notification payload: ' + payload);
selectNotificationSubject.add(payload);
}
selectNotificationSubject.add(payload);
});
print("Notifications initialised successfully");
}
@ -51,7 +51,7 @@ class NotificationClass{
Future<void> scheduleNotification(String title, String description,
notifs.FlutterLocalNotificationsPlugin notifsPlugin,
DateTime scheduledTime, String currentTimeZone, {int id, notifs.NotificationDetails platformChannelSpecifics}) async {
DateTime scheduledTime, String currentTimeZone, notifs.NotificationDetails platformChannelSpecifics, {int? id}) async {
if(id == null)
id = Random().nextInt(1000000);
// TODO: move to setup

View File

@ -7,7 +7,7 @@ class UserManager {
UserManager(this._storage);
Future<List<int>> loadLocalUserIds() async {
Future<List<int>?> loadLocalUserIds() async {
return await _storage.readAll().then((userMap) {
userMap.keys
.where((id) => _isNumeric(id))

View File

@ -7,26 +7,26 @@ import 'package:vikunja_app/models/user.dart';
@JsonSerializable()
class Bucket {
int id, listId, limit;
String? title;
double position;
String title;
DateTime created, updated;
User createdBy;
DateTime? created, updated;
User? createdBy;
bool isDoneBucket;
List<Task> tasks;
Bucket({
@required this.id,
@required this.listId,
required this.id,
required this.listId,
this.title,
this.position,
this.limit,
this.isDoneBucket,
this.position = 0,
required this.limit,
this.isDoneBucket = false,
this.created,
this.updated,
this.createdBy,
this.tasks,
this.tasks = const <Task>[],
});
List<Task> tasks = [];
Bucket.fromJSON(Map<String, dynamic> json)
: id = json['id'],
listId = json['list_id'],
@ -42,9 +42,9 @@ class Bucket {
? null
: User.fromJson(json['created_by']),
tasks = (json['tasks'] as List<dynamic>)
?.map((task) => Task.fromJson(task))
?.cast<Task>()
?.toList() ?? <Task>[];
.map((task) => Task.fromJson(task))
.cast<Task>()
.toList() ?? <Task>[];
toJSON() => {
'id': id,
@ -53,9 +53,9 @@ class Bucket {
'position': position,
'limit': limit,
'is_done_bucket': isDoneBucket ?? false,
'created': created?.toUtc()?.toIso8601String(),
'updated': updated?.toUtc()?.toIso8601String(),
'created': created?.toUtc().toIso8601String(),
'updated': updated?.toUtc().toIso8601String(),
'createdBy': createdBy?.toJSON(),
'tasks': tasks?.map((task) => task.toJSON())?.toList(),
'tasks': tasks?.map((task) => task.toJSON()).toList(),
};
}

View File

@ -5,13 +5,14 @@ import 'package:vikunja_app/theme/constants.dart';
class Label {
final int id;
final String title, description;
final DateTime created, updated;
final User createdBy;
final String? title, description;
final DateTime? created, updated;
final User? createdBy;
final Color color;
Label(
{this.id,
{
required this.id,
this.title,
this.description,
this.color = vLabelDefaultColor,
@ -24,7 +25,7 @@ class Label {
title = json['title'],
description = json['description'],
color = json['hex_color'] == ''
? null
? vLabelDefaultColor
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),

View File

@ -4,9 +4,9 @@ import 'package:vikunja_app/models/task.dart';
class LabelTask {
final Label label;
final Task task;
final Task? task;
LabelTask({@required this.label, @required this.task});
LabelTask({required this.label, required this.task});
LabelTask.fromJson(Map<String, dynamic> json)
: label = new Label(id: json['label_id']),

View File

@ -4,7 +4,7 @@ import 'package:vikunja_app/models/label.dart';
class LabelTaskBulk {
final List<Label> labels;
LabelTaskBulk({@required this.labels});
LabelTaskBulk({required this.labels});
LabelTaskBulk.fromJson(Map<String, dynamic> json)
: labels = json['labels']?.map((label) => Label.fromJson(label));

View File

@ -4,33 +4,33 @@ import 'package:vikunja_app/models/user.dart';
class TaskList {
final int id;
final String title, description;
final User owner;
final DateTime created, updated;
List<Task> tasks;
final String? title, description;
final User? owner;
final DateTime? created, updated;
List<Task?> tasks;
final bool isFavorite;
TaskList(
{@required this.id,
@required this.title,
{required this.id,
required this.title,
this.description,
this.owner,
this.created,
this.updated,
this.tasks,
this.isFavorite});
this.tasks = const <Task>[],
this.isFavorite = false});
TaskList.fromJson(Map<String, dynamic> json)
: id = json['id'],
owner = json['owner'] == null ? null : User.fromJson(json['owner']),
owner = json['owner'] == null ? null : User.fromJson(json['owner']),
description = json['description'],
title = json['title'],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
isFavorite = json['is_favorite'],
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();
.map((taskJson) => Task.fromJson(taskJson))
.toList();
toJSON() {
return {

View File

@ -3,15 +3,15 @@ import 'package:meta/meta.dart';
class Namespace {
final int id;
final DateTime created, updated;
final String title, description;
final User owner;
final DateTime? created, updated;
final String? title, description;
final User? owner;
Namespace(
{@required this.id,
{required this.id,
this.created,
this.updated,
@required this.title,
required this.title,
this.description,
this.owner});

View File

@ -5,16 +5,18 @@ 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? title, description, identifier;
final bool? done;
final Color? color;
final List<DateTime?>? reminderDates;
final String identifier;
final String? title, description;
final bool done;
final Color color;
final double? kanbanPosition;
final User? createdBy;
final Duration? repeatAfter;
@ -23,16 +25,14 @@ class Task {
final List<TaskAttachment>? attachments;
// TODO: add position(?)
// // TODO: use `late final` once upgraded to current dart version
late final CheckboxStatistics _checkboxStatistics;
bool loading = false;
// // TODO: use `late final` once upgraded to current dart version
Task({
@required this.id,
required this.id,
required this.identifier,
this.title,
this.description,
this.identifier,
this.done = false,
this.reminderDates,
this.dueDate,
@ -41,7 +41,7 @@ class Task {
this.parentTaskId,
this.priority,
this.repeatAfter,
this.color,
this.color = vBlue, // TODO: decide on color
this.kanbanPosition,
this.subtasks,
this.labels,
@ -53,16 +53,18 @@ class Task {
this.bucketId,
});
bool loading = false;
Task.fromJson(Map<String, dynamic> json)
: id = json['id'],
title = json['title'],
description = json['description'],
identifier = json['identifier'],
done = json['done'],
reminderDates = (json['reminder_dates'] as List<dynamic>)
?.map((ts) => DateTime.parse(ts))
?.cast<DateTime>()
?.toList(),
reminderDates = json['reminder_dates'] != null ? (json['reminder_dates'] as List<dynamic>)
.map((ts) => DateTime.parse(ts))
.cast<DateTime>()
.toList() : null,
dueDate = DateTime.parse(json['due_date']),
startDate = DateTime.parse(json['start_date']),
endDate = DateTime.parse(json['end_date']),
@ -70,23 +72,23 @@ class Task {
priority = json['priority'],
repeatAfter = Duration(seconds: json['repeat_after']),
color = json['hex_color'] == ''
? null
? vBlue
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
kanbanPosition = json['kanban_position'] is int
? json['kanban_position'].toDouble()
: json['kanban_position'],
labels = (json['labels'] as List<dynamic>)
?.map((label) => Label.fromJson(label))
?.cast<Label>()
?.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'] ?? []) 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(),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
listId = json['list_id'],
@ -102,13 +104,13 @@ class Task {
'identifier': identifier,
'done': done ?? false,
'reminder_dates':
reminderDates?.map((date) => date.toUtc().toIso8601String()).toList(),
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(),
@ -119,17 +121,15 @@ class Task {
'created': created?.toUtc().toIso8601String(),
};
Color? get textColor => color != null
? color!.computeLuminance() > 0.5 ? Colors.black : Colors.white
: null;
Color? get textColor => color.computeLuminance() > 0.5 ? Colors.black : Colors.white;
CheckboxStatistics get checkboxStatistics {
CheckboxStatistics? get checkboxStatistics {
if (_checkboxStatistics != null)
return _checkboxStatistics;
if (description.isEmpty)
if (description!.isEmpty)
return null;
_checkboxStatistics = getCheckboxStatistics(description);
_checkboxStatistics = getCheckboxStatistics(description!);
return _checkboxStatistics;
}
@ -141,22 +141,22 @@ class Task {
return false;
}
bool get hasDueDate => dueDate.year != 1;
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,
bool done,
Color color,
bool resetColor,
double kanbanPosition,
User createdBy,
Duration repeatAfter,
List<Task> subtasks,
List<Label> labels,
List<TaskAttachment> attachments,
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,
double? kanbanPosition,
User? createdBy,
Duration? repeatAfter,
List<Task>? subtasks,
List<Label>? labels,
List<TaskAttachment>? attachments,
}) {
return Task(
id: id ?? this.id,
@ -174,7 +174,7 @@ class Task {
description: description ?? this.description,
identifier: identifier ?? this.identifier,
done: done ?? this.done,
color: (resetColor ?? false) ? null : (color ?? this.color),
color: (resetColor ?? false) ? vBlue : (color ?? this.color),
kanbanPosition: kanbanPosition ?? this.kanbanPosition,
createdBy: createdBy ?? this.createdBy,
repeatAfter: repeatAfter ?? this.repeatAfter,

View File

@ -6,13 +6,13 @@ import 'package:vikunja_app/models/user.dart';
@JsonSerializable()
class TaskAttachment {
int id, taskId;
DateTime created;
User createdBy;
DateTime? created;
User? createdBy;
// TODO: add file
TaskAttachment({
@required this.id,
@required this.taskId,
required this.id,
required this.taskId,
this.created,
this.createdBy,
});

View File

@ -13,8 +13,8 @@ class User {
toJSON() => {"id": this.id, "email": this.email, "username": this.username};
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

@ -22,14 +22,14 @@ class HomePage extends StatefulWidget {
class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
List<Namespace> _namespaces = [];
Namespace get _currentNamespace =>
Namespace? get _currentNamespace =>
_selectedDrawerIndex >= 0 && _selectedDrawerIndex < _namespaces.length
? _namespaces[_selectedDrawerIndex]
: null;
int _selectedDrawerIndex = -1, _previousDrawerIndex = -1;
bool _loading = true;
bool _showUserDetails = false;
Widget drawerItem;
Widget? drawerItem;
@override
void afterFirstLayout(BuildContext context) {
@ -42,7 +42,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),
)));
@ -102,7 +102,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
context,
MaterialPageRoute(
builder: (context) => NamespaceEditPage(
namespace: _currentNamespace,
namespace: _currentNamespace!,
))).whenComplete(() => _loadNamespaces()))
],
),
@ -110,9 +110,9 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail:
currentUser?.email == null ? null : Text(currentUser.email),
currentUser?.email == null ? null : Text(currentUser?.email ?? ""),
accountName:
currentUser?.username == null ? null : Text(currentUser.username),
currentUser?.username == null ? null : Text(currentUser?.username ?? ""),
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
@ -189,7 +189,7 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
_addNamespace(String name, BuildContext context) {
VikunjaGlobal.of(context)
.namespaceService
.create(Namespace(id: null, title: name))
.create(Namespace(id: 0, title: name))
.then((_) {
_loadNamespaces();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(

View File

@ -12,7 +12,7 @@ import '../models/task.dart';
class LandingPage extends StatefulWidget {
const LandingPage(
{Key key})
{Key? key})
: super(key: key);
@override
@ -21,8 +21,8 @@ class LandingPage extends StatefulWidget {
}
class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingPage> {
int defaultList;
List<Task> _list;
int? defaultList;
List<Task>? _list;
static const platform = const MethodChannel('vikunja');
@ -69,7 +69,7 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
@override
Widget build(BuildContext context) {
if(_list == null || _list.isEmpty)
if(_list == null || _list!.isEmpty)
_loadList(context);
return new Scaffold(
body: RefreshIndicator(
@ -111,7 +111,7 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
_addTask(Task task, BuildContext context) {
var globalState = VikunjaGlobal.of(context);
globalState.taskService.add(defaultList, task).then((_) {
globalState.taskService.add(defaultList!, task).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'),
));
@ -121,7 +121,7 @@ class LandingPageState extends State<LandingPage> with AfterLayoutMixin<LandingP
List<Widget> _listTasks(BuildContext context) {
var tasks = (_list.map((task) => _buildTile(task, context)) ?? []).toList();
var tasks = (_list?.map((task) => _buildTile(task, context)) ?? []).toList();
//tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
}

View File

@ -30,14 +30,14 @@ class BucketProps {
bool scrollable = false;
bool portrait = true;
int bucketLength = 0;
Size taskDropSize;
Size? taskDropSize;
}
class ListPage extends StatefulWidget {
final TaskList taskList;
//ListPage({this.taskList}) : super(key: Key(taskList.id.toString()));
ListPage({this.taskList}) : super(key: Key(Random().nextInt(100000).toString()));
ListPage({required this.taskList}) : super(key: Key(Random().nextInt(100000).toString()));
@override
_ListPageState createState() => _ListPageState();
@ -46,15 +46,15 @@ class ListPage extends StatefulWidget {
class _ListPageState extends State<ListPage> {
final _keyboardController = KeyboardVisibilityController();
int _viewIndex = 0;
TaskList _list;
TaskList? _list;
List<Task> _loadingTasks = [];
int _currentPage = 1;
bool _loading = true;
bool displayDoneTasks;
ListProvider taskState;
PageController _pageController;
bool? displayDoneTasks;
ListProvider? taskState;
PageController? _pageController;
Map<int, BucketProps> _bucketProps = {};
int _draggedBucketIndex;
int? _draggedBucketIndex;
Duration _lastTaskDragUpdateAction = Duration.zero;
@override
@ -80,7 +80,7 @@ class _ListPageState extends State<ListPage> {
onTap: () => FocusScope.of(context).unfocus(),
child: Scaffold(
appBar: AppBar(
title: Text(_list.title),
title: Text(_list?.title ?? ""),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
@ -88,16 +88,16 @@ class _ListPageState extends State<ListPage> {
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
list: _list!,
),
)).whenComplete(() => _loadList()),
),
],
),
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
body: !taskState!.isLoading
? RefreshIndicator(
child: taskState.tasks.length > 0 || taskState.buckets.length > 0
child: taskState!.tasks.length > 0 || taskState!.buckets.length > 0
? ListenableProvider.value(
value: taskState,
child: Theme(
@ -163,6 +163,7 @@ class _ListPageState extends State<ListPage> {
ListView _listView(BuildContext context) {
return ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0),
itemCount: taskState!.tasks.length,
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
@ -173,19 +174,22 @@ 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
if (taskState.maxPages == _currentPage &&
index == taskState.tasks.length)
return null;
if (index >= taskState.tasks.length &&
_currentPage < taskState.maxPages) {
// 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");
if (index >= taskState!.tasks.length &&
_currentPage < taskState!.maxPages) {
_currentPage++;
_loadTasksForPage(_currentPage);
}
return index < taskState.tasks.length
? _buildTile(taskState.tasks[index])
: null;
return _buildTile(taskState!.tasks[index]);
}
);
}
@ -197,7 +201,7 @@ class _ListPageState extends State<ListPage> {
final bucketWidth = deviceData.size.width * bucketFraction;
if (_pageController == null) _pageController = PageController(viewportFraction: bucketFraction);
else if (_pageController.viewportFraction != bucketFraction)
else if (_pageController!.viewportFraction != bucketFraction)
_pageController = PageController(viewportFraction: bucketFraction);
return ReorderableListView.builder(
@ -205,17 +209,17 @@ class _ListPageState extends State<ListPage> {
scrollController: _pageController,
physics: PageScrollPhysics(),
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
itemCount: taskState.buckets.length,
itemCount: taskState?.buckets.length ?? 0,
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
if (index > taskState.buckets.length) return null;
if (index > (taskState!.buckets.length)) throw Exception("Check itemCount attribute");
return ReorderableDelayedDragStartListener(
key: ValueKey<int>(index),
index: index,
enabled: taskState.buckets.length > 1 && !taskState.taskDragging,
enabled: taskState!.buckets.length > 1 && !taskState!.taskDragging,
child: SizedBox(
width: bucketWidth,
child: _buildBucketTile(taskState.buckets[index], portrait),
child: _buildBucketTile(taskState!.buckets[index], portrait),
),
);
},
@ -257,37 +261,37 @@ class _ListPageState extends State<ListPage> {
onReorder: (_, __) {},
onReorderEnd: (newIndex) async {
bool indexUpdated = false;
if (newIndex > _draggedBucketIndex) {
if (newIndex > _draggedBucketIndex!) {
newIndex -= 1;
indexUpdated = true;
}
final movedBucket = taskState.buckets.removeAt(_draggedBucketIndex);
if (newIndex >= taskState.buckets.length) {
taskState.buckets.add(movedBucket);
final movedBucket = taskState!.buckets.removeAt(_draggedBucketIndex!);
if (newIndex >= taskState!.buckets.length) {
taskState!.buckets.add(movedBucket);
} else {
taskState.buckets.insert(newIndex, movedBucket);
taskState!.buckets.insert(newIndex, movedBucket);
}
taskState.buckets[newIndex].position = calculateItemPosition(
taskState!.buckets[newIndex].position = calculateItemPosition(
positionBefore: newIndex != 0
? taskState.buckets[newIndex - 1].position : null,
positionAfter: newIndex < taskState.buckets.length - 1
? taskState.buckets[newIndex + 1].position : null,
? taskState!.buckets[newIndex - 1].position : null,
positionAfter: newIndex < taskState!.buckets.length - 1
? taskState!.buckets[newIndex + 1].position : null,
);
await _updateBucket(context, taskState.buckets[newIndex]);
await _updateBucket(context, taskState!.buckets[newIndex]);
// make sure the first 2 buckets don't have 0 position
if (newIndex == 0 && taskState.buckets.length > 1 && taskState.buckets[1].position == 0) {
taskState.buckets[1].position = calculateItemPosition(
positionBefore: taskState.buckets[0].position,
positionAfter: 1 < taskState.buckets.length - 1
? taskState.buckets[2].position : null,
if (newIndex == 0 && taskState!.buckets.length > 1 && taskState!.buckets[1].position == 0) {
taskState!.buckets[1].position = calculateItemPosition(
positionBefore: taskState!.buckets[0].position,
positionAfter: 1 < taskState!.buckets.length - 1
? taskState!.buckets[2].position : null,
);
_updateBucket(context, taskState.buckets[1]);
_updateBucket(context, taskState!.buckets[1]);
}
if (indexUpdated && portrait) _pageController.animateToPage(
if (indexUpdated && portrait) _pageController!.animateToPage(
newIndex - 1,
duration: Duration(milliseconds: 100),
curve: Curves.easeInOut,
@ -331,22 +335,22 @@ class _ListPageState extends State<ListPage> {
if (_bucketProps[bucket.id] == null)
_bucketProps[bucket.id] = BucketProps();
if (_bucketProps[bucket.id].bucketLength != (bucket.tasks.length)
|| _bucketProps[bucket.id].portrait != portrait)
if (_bucketProps[bucket.id]!.bucketLength != (bucket.tasks.length)
|| _bucketProps[bucket.id]!.portrait != portrait)
SchedulerBinding.instance.addPostFrameCallback((_) {
if (_bucketProps[bucket.id].controller.hasClients) setState(() {
_bucketProps[bucket.id].bucketLength = bucket.tasks.length;
_bucketProps[bucket.id].scrollable = _bucketProps[bucket.id].controller.position.maxScrollExtent > 0;
_bucketProps[bucket.id].portrait = portrait;
if (_bucketProps[bucket.id]!.controller.hasClients) setState(() {
_bucketProps[bucket.id]!.bucketLength = bucket.tasks.length;
_bucketProps[bucket.id]!.scrollable = _bucketProps[bucket.id]!.controller.position.maxScrollExtent > 0;
_bucketProps[bucket.id]!.portrait = portrait;
});
});
if (_bucketProps[bucket.id].titleController.text.isEmpty)
_bucketProps[bucket.id].titleController.text = bucket.title;
if (_bucketProps[bucket.id]!.titleController.text.isEmpty)
_bucketProps[bucket.id]!.titleController.text = bucket.title ?? "";
return Stack(
children: <Widget>[
CustomScrollView(
controller: _bucketProps[bucket.id].controller,
controller: _bucketProps[bucket.id]!.controller,
slivers: <Widget>[
SliverBucketPersistentHeader(
minExtent: bucketTitleHeight,
@ -365,7 +369,7 @@ class _ListPageState extends State<ListPage> {
children: <Widget>[
Expanded(
child: TextField(
controller: _bucketProps[bucket.id].titleController,
controller: _bucketProps[bucket.id]!.titleController,
decoration: const InputDecoration.collapsed(
hintText: 'Bucket Title',
),
@ -389,7 +393,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?.copyWith(
color: bucket.limit != 0 && bucket.tasks.length >= bucket.limit
? Colors.red : null,
),
@ -426,7 +430,7 @@ class _ListPageState extends State<ListPage> {
}
},
itemBuilder: (context) {
final enableDelete = taskState.buckets.length > 1;
final bool enableDelete = (taskState?.buckets.length ?? 0) > 1;
return <PopupMenuEntry<BucketMenu>>[
PopupMenuItem<BucketMenu>(
value: BucketMenu.limit,
@ -479,22 +483,22 @@ class _ListPageState extends State<ListPage> {
child: SliverBucketList(
bucket: bucket,
onTaskDragUpdate: (details) { // scroll when dragging a task
if (details.sourceTimeStamp - _lastTaskDragUpdateAction > const Duration(milliseconds: 600)) {
if (details.sourceTimeStamp! - _lastTaskDragUpdateAction > const Duration(milliseconds: 600)) {
final screenSize = MediaQuery.of(context).size;
const scrollDuration = Duration(milliseconds: 250);
const scrollCurve = Curves.easeInOut;
final updateAction = () => setState(() => _lastTaskDragUpdateAction = details.sourceTimeStamp);
final updateAction = () => setState(() => _lastTaskDragUpdateAction = details.sourceTimeStamp!);
if (details.globalPosition.dx < screenSize.width * 0.1) { // scroll left
if (_pageController.position.extentBefore != 0)
_pageController.previousPage(duration: scrollDuration, curve: scrollCurve);
if (_pageController!.position.extentBefore != 0)
_pageController!.previousPage(duration: scrollDuration, curve: scrollCurve);
updateAction();
} else if (details.globalPosition.dx > screenSize.width * 0.9) { // scroll right
if (_pageController.position.extentAfter != 0)
_pageController.nextPage(duration: scrollDuration, curve: scrollCurve);
if (_pageController!.position.extentAfter != 0)
_pageController!.nextPage(duration: scrollDuration, curve: scrollCurve);
updateAction();
} else {
final viewingBucket = taskState.buckets[_pageController.page.floor()];
final bucketController = _bucketProps[viewingBucket.id].controller;
final viewingBucket = taskState!.buckets[_pageController!.page!.floor()];
final bucketController = _bucketProps[viewingBucket.id]!.controller;
if (details.globalPosition.dy < screenSize.height * 0.2) { // scroll up
if (bucketController.position.extentBefore != 0)
bucketController.animateTo(bucketController.offset - 80,
@ -513,7 +517,7 @@ class _ListPageState extends State<ListPage> {
),
),
SliverVisibility(
visible: !_bucketProps[bucket.id].scrollable,
visible: !_bucketProps[bucket.id]!.scrollable,
maintainState: true,
maintainAnimation: true,
maintainSize: true,
@ -523,9 +527,9 @@ class _ListPageState extends State<ListPage> {
children: <Widget>[
Column(
children: <Widget>[
if (_bucketProps[bucket.id].taskDropSize != null) DottedBorder(
if (_bucketProps[bucket.id]!.taskDropSize != null) DottedBorder(
color: Colors.grey,
child: SizedBox.fromSize(size: _bucketProps[bucket.id].taskDropSize),
child: SizedBox.fromSize(size: _bucketProps[bucket.id]!.taskDropSize),
),
Align(
alignment: Alignment.topCenter,
@ -536,7 +540,7 @@ class _ListPageState extends State<ListPage> {
// DragTarget to drop tasks in empty buckets
if (bucket.tasks.length == 0) DragTarget<TaskData>(
onWillAccept: (data) {
setState(() => _bucketProps[bucket.id].taskDropSize = data.size);
setState(() => _bucketProps[bucket.id]!.taskDropSize = data?.size);
return true;
},
onAccept: (data) {
@ -548,9 +552,9 @@ class _ListPageState extends State<ListPage> {
).then((_) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('${data.task.title} was moved to ${bucket.title} successfully!'),
)));
setState(() => _bucketProps[bucket.id].taskDropSize = null);
setState(() => _bucketProps[bucket.id]!.taskDropSize = null);
},
onLeave: (_) => setState(() => _bucketProps[bucket.id].taskDropSize = null),
onLeave: (_) => setState(() => _bucketProps[bucket.id]!.taskDropSize = null),
builder: (_, __, ___) => SizedBox.expand(),
),
],
@ -559,7 +563,7 @@ class _ListPageState extends State<ListPage> {
),
],
),
if (_bucketProps[bucket.id].scrollable) Align(
if (_bucketProps[bucket.id]!.scrollable) Align(
alignment: Alignment.bottomCenter,
child: addTaskButton,
),
@ -568,7 +572,7 @@ class _ListPageState extends State<ListPage> {
}
Future<void> updateDisplayDoneTasks() {
return VikunjaGlobal.of(context).listService.getDisplayDoneTasks(_list.id)
return VikunjaGlobal.of(context).listService.getDisplayDoneTasks(_list!.id)
.then((value) {displayDoneTasks = value == "1";});
}
@ -581,7 +585,7 @@ class _ListPageState extends State<ListPage> {
MaterialPageRoute(
builder: (context) => TaskEditPage(
task: task,
taskState: taskState,
taskState: taskState!,
),
),
),
@ -597,7 +601,7 @@ class _ListPageState extends State<ListPage> {
case 1:
await _loadBucketsForPage(1);
// load all buckets to get length for RecordableListView
while (_currentPage < taskState.maxPages) {
while (_currentPage < taskState!.maxPages) {
_currentPage++;
await _loadBucketsForPage(_currentPage);
}
@ -611,7 +615,7 @@ class _ListPageState extends State<ListPage> {
Future<void> _loadTasksForPage(int page) {
return Provider.of<ListProvider>(context, listen: false).loadTasks(
context: context,
listId: _list.id,
listId: _list!.id,
page: page,
displayDoneTasks: displayDoneTasks ?? false
);
@ -620,12 +624,12 @@ class _ListPageState extends State<ListPage> {
Future<void> _loadBucketsForPage(int page) {
return Provider.of<ListProvider>(context, listen: false).loadBuckets(
context: context,
listId: _list.id,
listId: _list!.id,
page: page
);
}
Future<void> _addItemDialog(BuildContext context, [Bucket bucket]) {
Future<void> _addItemDialog(BuildContext context, [Bucket? bucket]) {
return showDialog(
context: context,
builder: (_) => AddDialog(
@ -638,7 +642,7 @@ class _ListPageState extends State<ListPage> {
);
}
Future<void> _addItem(String title, BuildContext context, [Bucket bucket]) {
Future<void> _addItem(String title, BuildContext context, [Bucket? bucket]) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null,
@ -646,13 +650,14 @@ class _ListPageState extends State<ListPage> {
createdBy: globalState.currentUser,
done: false,
bucketId: bucket?.id,
identifier: '',
);
setState(() => _loadingTasks.add(newTask));
return Provider.of<ListProvider>(context, listen: false)
.addTask(
context: context,
newTask: newTask,
listId: _list.id,
listId: _list!.id,
)
.then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
@ -682,12 +687,13 @@ class _ListPageState extends State<ListPage> {
return Provider.of<ListProvider>(context, listen: false).addBucket(
context: context,
newBucket: Bucket(
id: null,
id: 0,
title: title,
createdBy: VikunjaGlobal.of(context).currentUser,
listId: _list.id,
listId: _list!.id,
limit: 0,
),
listId: _list.id,
listId: _list!.id,
).then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The bucket was added successfully!'),

View File

@ -9,7 +9,7 @@ import 'package:vikunja_app/theme/buttonText.dart';
class ListEditPage extends StatefulWidget {
final TaskList list;
ListEditPage({this.list}) : super(key: Key(list.toString()));
ListEditPage({required this.list}) : super(key: Key(list.toString()));
@override
State<StatefulWidget> createState() => _ListEditPageState();
@ -18,9 +18,9 @@ class ListEditPage extends StatefulWidget {
class _ListEditPageState extends State<ListEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String _title, _description;
bool displayDoneTasks;
int listId;
String? _title, _description;
bool? displayDoneTasks;
int listId = -1;
@override
void initState(){
@ -34,7 +34,7 @@ class _ListEditPageState extends State<ListEditPage> {
VikunjaGlobal.of(context).listService.getDisplayDoneTasks(listId).then(
(value) => setState(() => displayDoneTasks = value == "1"));
else
log("Display done tasks: " + displayDoneTasks?.toString());
log("Display done tasks: " + displayDoneTasks.toString());
return Scaffold(
appBar: AppBar(
title: Text('Edit List'),
@ -54,9 +54,9 @@ class _ListEditPageState extends State<ListEditPage> {
initialValue: widget.list.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.';
}
//if (title?.length < 3 || title.length > 250) {
// return 'The title needs to have between 3 and 250 characters.';
//}
return null;
},
decoration: new InputDecoration(
@ -73,6 +73,8 @@ class _ListEditPageState extends State<ListEditPage> {
initialValue: widget.list.description,
onSaved: (description) => _description = description,
validator: (description) {
if(description == null)
return null;
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
@ -115,12 +117,12 @@ class _ListEditPageState extends State<ListEditPage> {
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
if (_formKey.currentState!.validate()) {
Form.of(context)?.save();
_saveList(context);
}
}
: null,
: () {},
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),

View File

@ -16,11 +16,9 @@ class TaskEditPage extends StatefulWidget {
final ListProvider taskState;
TaskEditPage({
@required this.task,
@required this.taskState,
}) : assert(task != null),
assert(taskState != null),
super(key: Key(task.toString()));
required this.task,
required this.taskState,
}) : super(key: Key(task.toString()));
@override
State<StatefulWidget> createState() => _TaskEditPageState();
@ -32,18 +30,18 @@ class _TaskEditPageState extends State<TaskEditPage> {
bool _loading = false;
bool _changed = false;
int _priority;
DateTime _dueDate, _startDate, _endDate;
List<DateTime> _reminderDates;
String _title, _description, _repeatAfterType;
Duration _repeatAfter;
List<Label> _labels;
int? _priority;
DateTime? _dueDate, _startDate, _endDate;
List<DateTime?>? _reminderDates;
String? _title, _description, _repeatAfterType;
Duration? _repeatAfter;
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;
List<Label>? _suggestedLabels;
var _reminderInputs = <Widget>[];
final _labelTypeAheadController = TextEditingController();
Color _color;
Color _pickerColor;
Color? _color;
Color? _pickerColor;
bool _resetColor = false;
@override
@ -52,11 +50,11 @@ class _TaskEditPageState extends State<TaskEditPage> {
if (_reminderDates == null) {
_reminderDates = widget.task.reminderDates ?? [];
_reminderDates?.asMap()?.forEach((i, time) =>
setState(() => _reminderInputs?.add(VikunjaDateTimePicker(
_reminderDates!.asMap().forEach((i, time) =>
setState(() => _reminderInputs.add(VikunjaDateTimePicker(
initialValue: time,
label: 'Reminder',
onSaved: (reminder) => _reminderDates[i] = reminder,
onSaved: (reminder) { if(reminder == null) return null; _reminderDates![i] = reminder; },
)))
);
}
@ -68,7 +66,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
return WillPopScope(
onWillPop: () {
if(_changed) {
return _showConfirmationDialog();
return (_showConfirmationDialog() ?? Future.value(false));
}
return new Future(() => true);
},
@ -95,9 +93,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
onSaved: (title) => _title = title,
onChanged: (_) => _changed = true,
validator: (title) {
if (title.length < 3 || title.length > 250) {
return 'The title needs to have between 3 and 250 characters.';
}
//if (title.length < 3 || title.length > 250) {
// return 'The title needs to have between 3 and 250 characters.';
//}
return null;
},
decoration: new InputDecoration(
@ -115,6 +113,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
onSaved: (description) => _description = description,
onChanged: (_) => _changed = true,
validator: (description) {
if (description == null)
return null;
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
@ -170,7 +170,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
value: _repeatAfterType ??
getRepeatAfterTypeFromDuration(
widget.task.repeatAfter),
onChanged: (String newValue) {
onChanged: (String? newValue) {
setState(() {
_repeatAfterType = newValue;
});
@ -217,15 +217,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(null);
var currentIndex = _reminderDates!.length - 1;
// FIXME: Why does putting this into a row fails?
setState(() => _reminderInputs.add(
VikunjaDateTimePicker(
label: 'Reminder',
onSaved: (reminder) =>
_reminderDates[currentIndex] = reminder,
_reminderDates![currentIndex] = reminder,
onChanged: (_) => _changed = true,
initialValue: DateTime.now(),
),
@ -242,7 +242,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
value: _priorityToString(_priority),
isExpanded: true,
isDense: true,
onChanged: (String newValue) {
onChanged: (String? newValue) {
setState(() {
_priority = _priorityFromString(newValue);
});
@ -258,7 +258,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
),
Wrap(
spacing: 10,
children: _labels.map((Label label) {
children: _labels!.map((Label label) {
return LabelComponent(
label: label,
onDelete: () {
@ -277,14 +277,13 @@ class _TaskEditPageState extends State<TaskEditPage> {
InputDecoration(labelText: 'Add a new label')),
suggestionsCallback: (pattern) => _searchLabel(pattern),
itemBuilder: (context, suggestion) {
print(suggestion);
return new ListTile(title: Text(suggestion));
return new ListTile(title: Text(suggestion.toString()));
},
transitionBuilder: (context, suggestionsBox, controller) {
return suggestionsBox;
},
onSuggestionSelected: (suggestion) {
_addLabel(suggestion);
_addLabel(suggestion.toString());
},
),
),
@ -309,7 +308,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
ElevatedButton(
child: Text(
'Color',
style: _resetColor || (_color ?? widget.task.color) == null ? null : TextStyle(
style: _resetColor ? null : TextStyle(
color: (_color ?? widget.task.color)
.computeLuminance() > 0.5 ? Colors.black : Colors.white,
),
@ -323,8 +322,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
Padding(
padding: const EdgeInsets.only(left: 15),
child: () {
String colorString = (_resetColor ? null : (_color ?? widget.task.color))?.toString();
colorString = colorString?.substring(10, colorString.length - 1)?.toUpperCase();
String? colorString = (_resetColor ? null : (_color ?? widget.task.color))?.toString();
colorString = colorString?.substring(10, colorString.length - 1).toUpperCase();
colorString = colorString != null ? '#$colorString' : 'None';
return Text(
'$colorString',
@ -345,9 +344,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
),
floatingActionButton: FloatingActionButton(
onPressed: !_loading ? () {
if (_formKey.currentState.validate()) {
Form.of(_listKey.currentContext).save();
_saveTask(_listKey.currentContext);
if (_formKey.currentState!.validate()) {
Form.of(_listKey.currentContext!)!.save();
_saveTask(_listKey.currentContext!);
}
} : null,
child: Icon(Icons.save),
@ -361,7 +360,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 == null);
Task updatedTask = widget.task.copyWith(
title: _title,
@ -414,7 +413,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
_removeLabel(Label label) {
setState(() {
_labels.removeWhere((l) => l.id == label.id);
_labels?.removeWhere((l) => l.id == label.id);
});
}
@ -422,9 +421,9 @@ 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();
List<String?> labelText = labels.map((label) => label.title).toList();
return labelText;
});
}
@ -432,9 +431,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
_addLabel(String labelTitle) {
// FIXME: This is not an optimal solution...
bool found = false;
_suggestedLabels.forEach((label) {
_suggestedLabels?.forEach((label) {
if (label.title == labelTitle) {
_labels.add(label);
_labels?.add(label);
found = true;
}
});
@ -451,20 +450,20 @@ class _TaskEditPageState extends State<TaskEditPage> {
return;
}
Label newLabel = Label(title: labelTitle);
Label newLabel = Label(title: labelTitle, id: 0);
VikunjaGlobal.of(context)
.labelService
.create(newLabel)
.then((createdLabel) {
setState(() {
_labels.add(createdLabel);
_labels?.add(createdLabel);
_labelTypeAheadController.clear();
});
});
}
// FIXME: Move the following two functions to an extra class or type.
_priorityFromString(String priority) {
_priorityFromString(String? priority) {
switch (priority) {
case 'Low':
return 1;
@ -482,7 +481,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
}
}
_priorityToString(int priority) {
_priorityToString(int? priority) {
switch (priority) {
case 0:
return 'Unset';
@ -502,7 +501,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
}
_onColorEdit() {
_pickerColor = _resetColor || (_color ?? widget.task.color) == null
_pickerColor = _resetColor
? Colors.black
: _color ?? widget.task.color;
showDialog(
@ -511,7 +510,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
title: const Text('Task Color'),
content: SingleChildScrollView(
child: ColorPicker(
pickerColor: _pickerColor,
pickerColor: _pickerColor!,
enableAlpha: false,
labelTypes: const [ColorLabelType.hsl, ColorLabelType.rgb],
paletteType: PaletteType.hslWithLightness,
@ -557,7 +556,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
}
Future<bool> _showConfirmationDialog() async {
return showDialog<bool>(
return await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
@ -588,6 +587,6 @@ class _TaskEditPageState extends State<TaskEditPage> {
],
);
},
);
) ?? false;
}
}

View File

@ -15,7 +15,7 @@ import 'package:vikunja_app/stores/list_store.dart';
class NamespacePage extends StatefulWidget {
final Namespace namespace;
NamespacePage({this.namespace}) : super(key: Key(namespace.id.toString()));
NamespacePage({required this.namespace}) : super(key: Key(namespace.id.toString()));
@override
_NamespacePageState createState() => new _NamespacePageState();
@ -47,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),
),
@ -126,7 +126,7 @@ class _NamespacePageState extends State<NamespacePage>
_addList(String name, BuildContext context) {
VikunjaGlobal.of(context)
.listService
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: []))
.create(widget.namespace.id, TaskList(id: 0, title: name, tasks: []))
.then((_) {
setState(() {});
_loadLists();

View File

@ -7,7 +7,7 @@ import 'package:vikunja_app/theme/buttonText.dart';
class NamespaceEditPage extends StatefulWidget {
final Namespace namespace;
NamespaceEditPage({this.namespace}) : super(key: Key(namespace.toString()));
NamespaceEditPage({required this.namespace}) : super(key: Key(namespace.toString()));
@override
State<StatefulWidget> createState() => _NamespaceEditPageState();
@ -16,7 +16,7 @@ class NamespaceEditPage extends StatefulWidget {
class _NamespaceEditPageState extends State<NamespaceEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String _name, _description;
String? _name, _description;
@override
Widget build(BuildContext ctx) {
@ -39,9 +39,9 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
initialValue: widget.namespace.title,
onSaved: (name) => _name = name,
validator: (name) {
if (name.length < 3 || name.length > 250) {
return 'The name needs to have between 3 and 250 characters.';
}
//if (name.length < 3 || name.length > 250) {
// return 'The name needs to have between 3 and 250 characters.';
//}
return null;
},
decoration: new InputDecoration(
@ -58,9 +58,9 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
initialValue: widget.namespace.description,
onSaved: (description) => _description = description,
validator: (description) {
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
//if (description.length > 1000) {
// return 'The description can have a maximum of 1000 characters.';
//}
return null;
},
decoration: new InputDecoration(
@ -75,12 +75,12 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
if (_formKey.currentState!.validate()) {
Form.of(context)?.save();
_saveNamespace(context);
}
}
: null,
: () => null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),

View File

@ -10,9 +10,9 @@ class SettingsPage extends StatefulWidget {
}
class SettingsPageState extends State<SettingsPage> {
List<TaskList> taskListList;
int defaultList;
bool ignoreCertificates;
List<TaskList>? taskListList;
int? defaultList;
bool? ignoreCertificates;
@override
Widget build(BuildContext context) {
@ -31,10 +31,10 @@ class SettingsPageState extends State<SettingsPage> {
taskListList != null ?
ListTile(
title: Text("Default List"),
trailing: DropdownButton(
items: [DropdownMenuItem(child: Text("None"), value: null,), ...taskListList.map((e) => DropdownMenuItem(child: Text(e.title), value: e.id)).toList()],
trailing: DropdownButton<int>(
items: [DropdownMenuItem(child: Text("None"), value: null,), ...taskListList!.map((e) => DropdownMenuItem(child: Text(e.title ?? ""), value: e.id)).toList()],
value: defaultList,
onChanged: (value){
onChanged: (int? value){
setState(() => defaultList = value);
VikunjaGlobal.of(context).listService.setDefaultList(value);
},

View File

@ -41,8 +41,8 @@ class _LoginPageState extends State<LoginPage> {
content: Text(
"Login has expired. Please reenter your details!")));
setState(() {
_serverController.text = VikunjaGlobal.of(context)?.client?.base;
_usernameController.text = VikunjaGlobal.of(context)?.currentUser?.username;
_serverController.text = VikunjaGlobal.of(context).client.base ?? "";
_usernameController.text = VikunjaGlobal.of(context).currentUser?.username ?? "";
});
}
});
@ -86,7 +86,7 @@ class _LoginPageState extends State<LoginPage> {
controller: _serverController,
autocorrect: false,
validator: (address) {
return isUrl(address) || address.isEmpty ? null : 'Invalid URL';
return (isUrl(address) || address != null || address!.isEmpty) ? null : 'Invalid URL';
},
decoration: new InputDecoration(
border: OutlineInputBorder(),
@ -118,7 +118,7 @@ class _LoginPageState extends State<LoginPage> {
padding: vStandardVerticalPadding,
child: CheckboxListTile(
value: _rememberMe,
onChanged: (value) => setState( () =>_rememberMe = value),
onChanged: (value) => setState( () => _rememberMe = value ?? false),
title: Text("Remember me"),
),
),
@ -126,12 +126,12 @@ class _LoginPageState extends State<LoginPage> {
builder: (context) => FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
if (_formKey.currentState!.validate()) {
Form.of(context)?.save();
_loginUser(context);
}
}
: null,
: () => null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Login'),
@ -146,7 +146,7 @@ class _LoginPageState extends State<LoginPage> {
)),
Builder(builder: (context) => FancyButton(
onPressed: () {
if(_formKey.currentState.validate() && _serverController.text.isNotEmpty) {
if(_formKey.currentState!.validate() && _serverController.text.isNotEmpty) {
Navigator.push(
context,
MaterialPageRoute(builder: (context) =>
@ -158,9 +158,9 @@ class _LoginPageState extends State<LoginPage> {
child: VikunjaButtonText("Login with Frontend"))),
client.ignoreCertificates != null ?
CheckboxListTile(title: Text("Ignore Certificates"), value: client.ignoreCertificates, onChanged: (value) {
setState(() => client.reload_ignore_certs(value));
VikunjaGlobal.of(context).settingsManager.setIgnoreCertificates(value);
VikunjaGlobal.of(context).client.ignoreCertificates = value;
setState(() => client.reload_ignore_certs(value ?? false));
VikunjaGlobal.of(context).settingsManager.setIgnoreCertificates(value ?? false);
VikunjaGlobal.of(context).client.ignoreCertificates = value ?? false;
}) : ListTile(title: Text("..."))
],
),
@ -187,14 +187,14 @@ class _LoginPageState extends State<LoginPage> {
Server info = await vGlobal.serverService.getInfo();
UserTokenPair newUser;
UserTokenPair? newUser;
try {
newUser =
await vGlobal.newUserService.login(
await vGlobal.newUserService?.login(
_username, _password, rememberMe: this._rememberMe);
} catch (e) {
if (e.runtimeType == InvalidRequestApiException && e.errorCode == 412) {
if (e is ApiException && e.errorCode == 412) {
TextEditingController totpController = TextEditingController();
await showDialog(context: context, builder: (context) =>
new AlertDialog(
@ -208,14 +208,15 @@ class _LoginPageState extends State<LoginPage> {
],
));
newUser =
await vGlobal.newUserService.login(
await vGlobal.newUserService?.login(
_username, _password, rememberMe: this._rememberMe,
totp: totpController.text);
} else {
throw e;
}
}
vGlobal.changeUser(newUser.user, token: newUser.token, base: _server);
if(newUser != null)
vGlobal.changeUser(newUser.user, token: newUser.token, base: _server);
} catch (ex, stacktrace) {
log(stacktrace.toString());
throw ex;
@ -244,8 +245,9 @@ class _LoginPageState extends State<LoginPage> {
vGS.client.configure(token: baseTokenPair.token, base: baseTokenPair.base, authenticated: true);
setState(() => _loading = true);
try {
var newUser = await vGS.newUserService.getCurrentUser();
vGS.changeUser(newUser, token: baseTokenPair.token, base: baseTokenPair.base);
var newUser = await vGS.newUserService?.getCurrentUser();
if(newUser != null)
vGS.changeUser(newUser, token: baseTokenPair.token, base: baseTokenPair.base);
} catch (e) {
log(e.toString());
}

View File

@ -17,8 +17,8 @@ class LoginWithWebView extends StatefulWidget {
class LoginWithWebViewState extends State<LoginWithWebView> {
WebView webView;
WebViewController webViewController;
WebView? webView;
WebViewController? webViewController;
@override
void initState() {
@ -30,7 +30,7 @@ class LoginWithWebViewState extends State<LoginWithWebView> {
userAgent: "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Mobile Safari/537.36",
onWebViewCreated: (controller) {
webViewController = controller;
webViewController.runJavascript("localStorage.clear(); location.href=location.href;");
webViewController!.runJavascript("localStorage.clear(); location.href=location.href;");
},
);
}
@ -42,19 +42,22 @@ class LoginWithWebViewState extends State<LoginWithWebView> {
body: webView
),
onWillPop: () async {
String currentUrl = await webViewController.currentUrl();
bool hasPopped = await _handlePageFinished(currentUrl);
return Future.value(!hasPopped);
String? currentUrl = await webViewController?.currentUrl();
if (currentUrl != null) {
bool hasPopped = await _handlePageFinished(currentUrl);
return Future.value(!hasPopped);
}
return Future.value(false);
},);
}
Future<bool> _handlePageFinished(String pageLocation) async {
log("handlePageFinished");
if(webViewController != null) {
String localStorage = await webViewController
String localStorage = await webViewController!
.runJavascriptReturningResult("JSON.stringify(localStorage);");
String apiUrl = await webViewController.runJavascriptReturningResult("API_URL");
String apiUrl = await webViewController!.runJavascriptReturningResult("API_URL");
if (localStorage != "{}") {
apiUrl = apiUrl.replaceAll("\"", "");
if(!apiUrl.startsWith("http")) {

View File

@ -13,7 +13,7 @@ class RegisterPage extends StatefulWidget {
class _RegisterPageState extends State<RegisterPage> {
final _formKey = GlobalKey<FormState>();
final passwordController = TextEditingController();
String _server, _username, _email, _password;
String? _server, _username, _email, _password;
bool _loading = false;
@override
@ -46,9 +46,9 @@ class _RegisterPageState extends State<RegisterPage> {
Padding(
padding: vStandardVerticalPadding,
child: TextFormField(
onSaved: (username) => _username = username.trim(),
onSaved: (username) => _username = username?.trim(),
validator: (username) {
return username.trim().isNotEmpty
return username!.trim().isNotEmpty
? null
: 'Please specify a username';
},
@ -77,7 +77,7 @@ class _RegisterPageState extends State<RegisterPage> {
controller: passwordController,
onSaved: (password) => _password = password,
validator: (password) {
return password.length >= 8
return (password?.length ?? 0) >= 8
? null
: 'Please use at least 8 characters';
},
@ -105,14 +105,14 @@ class _RegisterPageState extends State<RegisterPage> {
builder: (context) => FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
if (_formKey.currentState!.validate()) {
Form.of(context)?.save();
_registerUser(context);
} else {
print("awhat");
}
}
: null,
: () => null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Register'),
@ -129,9 +129,10 @@ class _RegisterPageState extends State<RegisterPage> {
var vGlobal = VikunjaGlobal.of(context);
var newUserLoggedIn = await vGlobal
.newUserService
.register(_username, _email, _password);
vGlobal.changeUser(newUserLoggedIn.user,
token: newUserLoggedIn.token, base: _server);
?.register(_username!, _email, _password);
if(newUserLoggedIn != null)
vGlobal.changeUser(newUserLoggedIn.user,
token: newUserLoggedIn.token, base: _server!);
} catch (ex) {
showDialog(
context: context,

View File

@ -45,6 +45,7 @@ var _tasks = {
created: DateTime.now(),
description: 'A descriptive task',
done: false,
identifier: '',
)
};
@ -84,7 +85,7 @@ class MockedNamespaceService implements NamespaceService {
class MockedListService implements ListService {
@override
Future<TaskList> create(namespaceId, TaskList tl) {
_nsLists[namespaceId].add(tl.id);
_nsLists[namespaceId]?.add(tl.id);
return Future.value(_lists[tl.id] = tl);
}
@ -107,7 +108,7 @@ class MockedListService implements ListService {
@override
Future<List<TaskList>> getByNamespace(int namespaceId) {
return Future.value(
_nsLists[namespaceId].map((listId) => _lists[listId]).toList());
_nsLists[namespaceId]!.map((listId) => _lists[listId]!).toList());
}
@override
@ -135,7 +136,7 @@ class MockedListService implements ListService {
}
@override
void setDefaultList(int listId) {
void setDefaultList(int? listId) {
// TODO: implement setDefaultList
}
}
@ -144,7 +145,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();
}
@ -152,25 +153,25 @@ 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] = task);
return Future.value(_tasks[task.id ?? 0] = task);
}
@override
Future<Task> add(int listId, Task task) {
var id = _tasks.keys.last + 1;
_tasks[id] = task;
_lists[listId].tasks.add(task);
_lists[listId]!.tasks.add(task);
return Future.value(task);
}
@override
Future<Response> getAllByList(int listId,
[Map<String, List<String>> queryParameters]) {
[Map<String, List<String>>? queryParameters]) {
return Future.value(new Response(_tasks.values.toList(), 200, {}));
}
@ -196,13 +197,13 @@ class MockedTaskService implements TaskService {
class MockedUserService implements UserService {
@override
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String totp}) {
return Future.value(UserTokenPair(_users[1], 'abcdefg'));
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String? totp}) {
return Future.value(UserTokenPair(_users[1]!, 'abcdefg'));
}
@override
Future<UserTokenPair> register(String username, email, password) {
return Future.value(UserTokenPair(_users[1], 'abcdefg'));
return Future.value(UserTokenPair(_users[1]!, 'abcdefg'));
}
@override

View File

@ -43,7 +43,7 @@ class TaskServiceOption<T> {
}
class TaskServiceOptions {
List<TaskServiceOption> options;
List<TaskServiceOption>? options;
TaskServiceOptions({this.options}) {
if(this.options == null)
@ -58,16 +58,18 @@ class TaskServiceOptions {
}
void setOption(TaskServiceOption option, dynamic value) {
options.firstWhere((element) => element.name == option.name).value = value;
options?.firstWhere((element) => element.name == option.name).value = value;
}
String getOptions() {
String result = '';
for(TaskServiceOption option in options) {
if(options == null)
return '';
for(TaskServiceOption option in options!) {
dynamic value = option.getValue();
if (value is List) {
for (dynamic value_entry in value) {
result += '&' + option.name + '[]=' + value_entry;
for (dynamic valueEntry in value) {
result += '&' + option.name + '[]=' + valueEntry;
}
} else {
result += '&' + option.name + '=' + value;
@ -98,7 +100,7 @@ abstract class ListService {
Future<String> getDisplayDoneTasks(int listId);
void setDisplayDoneTasks(int listId, String value);
Future<String?> getDefaultList();
void setDefaultList(int listId);
void setDefaultList(int? listId);
}
abstract class TaskService {
@ -168,7 +170,7 @@ class SettingsManager {
_storage.write(key: key, value: value);
});});}
Future<String> getIgnoreCertificates() {
Future<String?> getIgnoreCertificates() {
return _storage.read(key: "ignore-certificates");
}

View File

@ -38,7 +38,7 @@ class ListProvider with ChangeNotifier {
List<Bucket> get buckets => _buckets;
Future<void> loadTasks({BuildContext context, int listId, int page = 1, bool displayDoneTasks = true}) {
Future<void> loadTasks({required BuildContext context, required int listId, int page = 1, bool displayDoneTasks = true}) {
_tasks = [];
_isLoading = true;
notifyListeners();
@ -57,7 +57,7 @@ class ListProvider with ChangeNotifier {
}
return VikunjaGlobal.of(context).taskService.getAllByList(listId, queryParams).then((response) {
if (response.headers["x-pagination-total-pages"] != null) {
_maxPages = int.parse(response.headers["x-pagination-total-pages"]);
_maxPages = int.parse(response.headers["x-pagination-total-pages"]!);
}
_tasks.addAll(response.body);
@ -66,7 +66,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> loadBuckets({BuildContext context, int listId, int page = 1}) {
Future<void> loadBuckets({required BuildContext context, required int listId, int page = 1}) {
_buckets = [];
_isLoading = true;
notifyListeners();
@ -77,7 +77,7 @@ class ListProvider with ChangeNotifier {
return VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) {
if (response.headers["x-pagination-total-pages"] != null) {
_maxPages = int.parse(response.headers["x-pagination-total-pages"]);
_maxPages = int.parse(response.headers["x-pagination-total-pages"]!);
}
_buckets.addAll(response.body);
@ -87,10 +87,11 @@ class ListProvider with ChangeNotifier {
}
Future<void> addTaskByTitle(
{BuildContext context, String title, int listId}) {
{required BuildContext context, required String title, required int listId}) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null,
id: 0,
identifier: '',
title: title,
createdBy: globalState.currentUser,
done: false,
@ -105,7 +106,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> addTask({BuildContext context, Task newTask, int listId}) {
Future<void> addTask({required BuildContext context, required Task newTask, required int listId}) {
var globalState = VikunjaGlobal.of(context);
if (newTask.bucketId == null) _isLoading = true;
notifyListeners();
@ -126,7 +127,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<Task> updateTask({BuildContext context, Task task}) {
Future<Task> updateTask({required BuildContext context, required Task task}) {
return VikunjaGlobal.of(context).taskService.update(task).then((task) {
// FIXME: This is ugly. We should use a redux to not have to do these kind of things.
// This is enough for now (it works) but we should definitly fix it later.
@ -145,7 +146,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> addBucket({BuildContext context, Bucket newBucket, int listId}) {
Future<void> addBucket({required BuildContext context, required Bucket newBucket, required int listId}) {
notifyListeners();
return VikunjaGlobal.of(context).bucketService.add(listId, newBucket)
.then((bucket) {
@ -154,7 +155,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> updateBucket({BuildContext context, Bucket bucket}) {
Future<void> updateBucket({required BuildContext context, required Bucket bucket}) {
return VikunjaGlobal.of(context).bucketService.update(bucket)
.then((rBucket) {
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
@ -163,7 +164,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> deleteBucket({BuildContext context, int listId, int bucketId}) {
Future<void> deleteBucket({required BuildContext context, required int listId, required int bucketId}) {
return VikunjaGlobal.of(context).bucketService.delete(listId, bucketId)
.then((_) {
_buckets.removeWhere((bucket) => bucket.id == bucketId);
@ -171,7 +172,7 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> moveTaskToBucket({BuildContext context, Task task, int newBucketId, int index}) async {
Future<void> moveTaskToBucket({required BuildContext context, required Task task, required 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--;
@ -194,7 +195,7 @@ class ListProvider with ChangeNotifier {
_buckets[newBucketIndex].tasks[index] = task;
// make sure the first 2 tasks don't have 0 kanbanPosition
Task secondTask;
Task? secondTask;
if (index == 0 && _buckets[newBucketIndex].tasks.length > 1
&& _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) {
secondTask = await VikunjaGlobal.of(context).taskService.update(
@ -210,11 +211,12 @@ 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;
if (secondTask != null)
_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.sort((a, b) => a.kanbanPosition.compareTo(b.kanbanPosition));
_buckets[newBucketIndex].tasks.sort((a, b) => a.kanbanPosition!.compareTo(b.kanbanPosition!));
notifyListeners();
}

View File

@ -4,12 +4,12 @@ import 'package:vikunja_app/theme/constants.dart';
class FancyButton extends StatelessWidget {
final double width;
final double height;
final Function onPressed;
final VoidCallback? onPressed;
final Widget child;
const FancyButton({
Key key,
@required this.child,
Key? key,
required this.child,
this.width = double.infinity,
this.height = 35,
this.onPressed,

View File

@ -6,7 +6,7 @@ class VikunjaButtonText extends StatelessWidget {
const VikunjaButtonText(
this.text, {
Key key,
Key? key,
}) : super(key: key);
@override

View File

@ -26,7 +26,7 @@ ThemeData _buildVikunjaTheme(ThemeData base) {
// title: base.textTheme.title.copyWith(
// fontFamily: 'Quicksand',
// ),
button: base.textTheme.button.copyWith(
button: base.textTheme.button?.copyWith(
color:
vWhite, // This does not work, looks like a bug in Flutter: https://github.com/flutter/flutter/issues/19623
),

View File

@ -1,6 +1,6 @@
import 'dart:math';
double calculateItemPosition({double positionBefore, double positionAfter}) {
double calculateItemPosition({double? positionBefore, double? positionAfter}) {
// only
if (positionBefore == null && positionAfter == null) {
return 0;
@ -17,5 +17,5 @@ double calculateItemPosition({double positionBefore, double positionAfter}) {
}
// in the middle (positionBefore != null && positionAfter != null)
return (positionBefore + positionAfter) / 2;
return (positionBefore! + positionAfter!) / 2;
}

View File

@ -5,8 +5,8 @@ class CheckboxStatistics {
final int checked;
const CheckboxStatistics({
@required this.total,
@required this.checked,
required this.total,
required this.checked,
});
}
@ -15,8 +15,8 @@ class MatchedCheckboxes {
final Iterable<Match> unchecked;
const MatchedCheckboxes({
@required this.checked,
@required this.unchecked,
required this.checked,
required this.unchecked,
});
}
@ -28,7 +28,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))
checked.add(match);
else
unchecked.add(match);

View File

@ -1,4 +1,4 @@
getRepeatAfterTypeFromDuration(Duration repeatAfter) {
getRepeatAfterTypeFromDuration(Duration? repeatAfter) {
if (repeatAfter == null || repeatAfter.inSeconds == 0) {
return null;
}
@ -18,7 +18,7 @@ getRepeatAfterTypeFromDuration(Duration repeatAfter) {
return 'Hours';
}
getRepeatAfterValueFromDuration(Duration repeatAfter) {
getRepeatAfterValueFromDuration(Duration? repeatAfter) {
if (repeatAfter == null || repeatAfter.inSeconds == 0) {
return null;
}
@ -43,7 +43,7 @@ getRepeatAfterValueFromDuration(Duration repeatAfter) {
return repeatAfter.inHours;
}
getDurationFromType(String value, String type) {
getDurationFromType(String? value, String? type) {
// Return an empty duration if either of the values is not set
if (value == null || value == '' || type == null || type == '') {
return Duration();

View File

@ -720,7 +720,7 @@ packages:
name: xdg_directories
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
version: "0.2.0+2"
xml:
dependency: transitive
description:

View File

@ -4,7 +4,7 @@ description: Vikunja as Flutter cross platform app
version: 0.2.0+200099
environment:
sdk: ">=2.6.0 <3.0.0"
sdk: ">=2.12.0 <3.0.0"
dependencies:
flutter: