fix: warnings (#1)

* Ran make format

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* Add VS Code launch config

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* pages/list/list.dart: Stop spinning wheel after adding a task

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* stores/list_store.dart: Fix updateTask() not being a future

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* Replace FlatButton with TextButton widgets

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* components/TaskTile.dart: Remove dead code

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* theme/theme.dart: Fix accentColor deprecation

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* pages/list/list_edit.dart: Fix SnackBar.hideCurrentSnackBar() deprecation

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>

* Remove unused folder lib/managers

Signed-off-by: Timo Reichl <timo.reichl@mailbox.org>
This commit is contained in:
Timo Reichl 2021-12-21 12:22:17 +01:00 committed by GitHub
parent d778f102a7
commit ee99869cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 195 additions and 217 deletions

4
.gitignore vendored
View File

@ -31,6 +31,7 @@
.pub/
/build/
ios/Flutter/flutter_export_environment.sh
analysis_options.yaml
# Web related
lib/generated_plugin_registrant.dart
@ -45,3 +46,6 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# VS Code
.vscode/settings.json

10
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,10 @@
{
"configurations": [
{
"name": "Flutter",
"request": "launch",
"type": "dart",
"flutterMode": "debug"
}
]
}

View File

@ -42,28 +42,36 @@ class Client {
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);
return http.get(newUri, headers: _headers).then(_handleResponse);
}
Future<Response> delete(String url) {
return http.delete('${this.base}$url'.toUri(),
headers: _headers,
).then(_handleResponse);
return http
.delete(
'${this.base}$url'.toUri(),
headers: _headers,
)
.then(_handleResponse);
}
Future<Response> post(String url, {dynamic body}) {
return http.post('${this.base}$url'.toUri(),
headers: _headers,
body: _encoder.convert(body),
).then(_handleResponse);
return http
.post(
'${this.base}$url'.toUri(),
headers: _headers,
body: _encoder.convert(body),
)
.then(_handleResponse);
}
Future<Response> put(String url, {dynamic body}) {
return http.put('${this.base}$url'.toUri(),
headers: _headers,
body: _encoder.convert(body),
).then(_handleResponse);
return http
.put(
'${this.base}$url'.toUri(),
headers: _headers,
body: _encoder.convert(body),
)
.then(_handleResponse);
}
Response _handleResponse(http.Response response) {
@ -81,10 +89,7 @@ class Client {
response.statusCode, response.request.url.toString());
}
return Response(
_decoder.convert(response.body),
response.statusCode,
response.headers
);
_decoder.convert(response.body), response.statusCode, response.headers);
}
}

View File

@ -9,14 +9,16 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
@override
Future<Label> create(LabelTask lt) async {
return client.put('/tasks/${lt.task.id}/labels', body: lt.toJSON())
return client
.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}')
.then((result) => Label.fromJson(result.body));
return client
.delete('/tasks/${lt.task.id}/labels/${lt.label.id}')
.then((result) => Label.fromJson(result.body));
}
@override
@ -27,4 +29,4 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
return client.get('/tasks/${lt.task.id}/labels$params').then(
(label) => convertList(label, (result) => Label.fromJson(result)));
}
}
}

View File

@ -14,7 +14,7 @@ class LabelTaskBulkAPIService extends APIService
return client
.post('/tasks/${task.id}/labels/bulk',
body: LabelTaskBulk(labels: labels).toJSON())
.then((response) =>
convertList(response.body['labels'], (result) => Label.fromJson(result)));
.then((response) => convertList(
response.body['labels'], (result) => Label.fromJson(result)));
}
}

View File

@ -8,19 +8,22 @@ class LabelAPIService extends APIService implements LabelService {
@override
Future<Label> create(Label label) {
return client.put('/labels', body: label.toJSON())
return client
.put('/labels', body: label.toJSON())
.then((response) => Label.fromJson(response.body));
}
@override
Future<Label> delete(Label label) {
return client.delete('/labels/${label.id}')
return client
.delete('/labels/${label.id}')
.then((response) => Label.fromJson(response.body));
}
@override
Future<Label> get(int labelID) {
return client.get('/labels/$labelID')
return client
.get('/labels/$labelID')
.then((response) => Label.fromJson(response.body));
}

View File

@ -30,9 +30,9 @@ class ListAPIService extends APIService implements ListService {
return client.get('/lists/$listId').then((response) {
final map = response.body;
if (map.containsKey('id')) {
return client.get("/lists/$listId/tasks")
.then((tasks) => TaskList.fromJson(
map, tasksJson: tasks.body));
return client
.get("/lists/$listId/tasks")
.then((tasks) => TaskList.fromJson(map, tasksJson: tasks.body));
}
return TaskList.fromJson(map);
});

View File

@ -21,11 +21,11 @@ class AddDialog extends StatelessWidget {
)
]),
actions: <Widget>[
new FlatButton(
new TextButton(
child: const Text('CANCEL'),
onPressed: () => Navigator.pop(context),
),
new FlatButton(
new TextButton(
child: const Text('ADD'),
onPressed: () {
if (this.onAdd != null && textController.text.isNotEmpty)

View File

@ -10,7 +10,7 @@ class ErrorDialog extends StatelessWidget {
return AlertDialog(
content: Text(error.toString()),
actions: <Widget>[
FlatButton(
TextButton(
child: Text('Close'),
onPressed: () => Navigator.of(context).maybePop(),
)

View File

@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/task.dart';
class TaskTile extends StatefulWidget {
@ -21,17 +20,16 @@ class TaskTile extends StatefulWidget {
@override
TaskTileState createState() {
return new TaskTileState(this.task, this.loading);
return new TaskTileState(this.loading);
}
}
class TaskTileState extends State<TaskTile> {
bool _loading;
Task _currentTask;
TaskTileState(this._currentTask, this._loading)
: assert(_currentTask != null),
assert(_loading != null);
TaskTileState(this._loading) {
assert(_loading != null);
}
@override
Widget build(BuildContext context) {
@ -47,9 +45,10 @@ class TaskTileState extends State<TaskTile> {
)),
),
title: Text(widget.task.title),
subtitle: widget.task.description == null || widget.task.description.isEmpty
? null
: Text(widget.task.description),
subtitle:
widget.task.description == null || widget.task.description.isEmpty
? null
: Text(widget.task.description),
trailing: IconButton(
icon: Icon(Icons.settings),
onPressed: () => widget.onEdit,
@ -60,9 +59,10 @@ class TaskTileState extends State<TaskTile> {
title: Text(widget.task.title),
controlAffinity: ListTileControlAffinity.leading,
value: widget.task.done ?? false,
subtitle: widget.task.description == null || widget.task.description.isEmpty
? null
: Text(widget.task.description),
subtitle:
widget.task.description == null || widget.task.description.isEmpty
? null
: Text(widget.task.description),
secondary: IconButton(
icon: Icon(Icons.settings),
onPressed: widget.onEdit,
@ -70,28 +70,6 @@ class TaskTileState extends State<TaskTile> {
onChanged: widget.onMarkedAsDone,
);
}
void _change(bool value) async {
setState(() {
this._loading = true;
});
Task newTask = await _updateTask(widget.task, value);
setState(() {
//this.widget.task = newTask;
this._loading = false;
});
}
Future<Task> _updateTask(Task task, bool checked) {
// TODO use copyFrom
return VikunjaGlobal.of(context).taskService.update(Task(
id: task.id,
done: checked,
title: task.title,
description: task.description,
createdBy: null,
));
}
}
typedef Future<void> TaskChanged(Task task, bool newValue);

View File

@ -43,7 +43,9 @@ class VikunjaDateTimePicker extends StatelessWidget {
return showDatePicker(
context: context,
firstDate: DateTime(1900),
initialDate: currentValue.millisecondsSinceEpoch > 0 ? currentValue : DateTime.now(),
initialDate: currentValue.millisecondsSinceEpoch > 0
? currentValue
: DateTime.now(),
lastDate: DateTime(2100));
},
);

View File

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

View File

@ -8,7 +8,6 @@ import 'package:vikunja_app/api/list_implementation.dart';
import 'package:vikunja_app/api/namespace_implementation.dart';
import 'package:vikunja_app/api/task_implementation.dart';
import 'package:vikunja_app/api/user_implementation.dart';
import 'package:vikunja_app/managers/user.dart';
import 'package:vikunja_app/models/user.dart';
import 'package:vikunja_app/service/services.dart';
@ -22,7 +21,8 @@ class VikunjaGlobal extends StatefulWidget {
VikunjaGlobalState createState() => VikunjaGlobalState();
static VikunjaGlobalState of(BuildContext context) {
var widget = context.dependOnInheritedWidgetOfExactType<_VikunjaGlobalInherited>();
var widget =
context.dependOnInheritedWidgetOfExactType<_VikunjaGlobalInherited>();
return widget.data;
}
}
@ -38,8 +38,6 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
Client get client => _client;
UserManager get userManager => new UserManager(_storage);
UserService newUserService(base) => new UserAPIService(Client(null, base));
NamespaceService get namespaceService => new NamespaceAPIService(client);

View File

@ -1,24 +0,0 @@
import 'dart:async';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class UserManager {
final FlutterSecureStorage _storage;
UserManager(this._storage);
Future<List<int>> loadLocalUserIds() async {
return await _storage.readAll().then((userMap) {
userMap.keys
.where((id) => _isNumeric(id))
.map((idString) => int.tryParse(idString));
});
}
bool _isNumeric(String str) {
if (str == null) {
return false;
}
return double.tryParse(str) != null;
}
}

View File

@ -1,6 +1,5 @@
import 'dart:ui';
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/user.dart';
class Label {

View File

@ -3,7 +3,6 @@ import 'package:json_annotation/json_annotation.dart';
import 'package:vikunja_app/models/label.dart';
import 'package:vikunja_app/models/user.dart';
import 'package:vikunja_app/utils/datetime_to_unix.dart';
@JsonSerializable()
class Task {
@ -60,16 +59,17 @@ class Task {
?.toList(),
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
createdBy = json['created_by'] == null ? null : User.fromJson(json['created_by']);
createdBy = json['created_by'] == null
? null
: User.fromJson(json['created_by']);
toJSON() => {
'id': id,
'title': title,
'description': description,
'done': done ?? false,
'reminder_dates': reminderDates
?.map((date) => date?.toIso8601String())
?.toList(),
'reminder_dates':
reminderDates?.map((date) => date?.toIso8601String())?.toList(),
'due_date': dueDate?.toIso8601String(),
'start_date': startDate?.toIso8601String(),
'end_date': endDate?.toIso8601String(),

View File

@ -14,8 +14,7 @@ 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}";
return VikunjaGlobal.of(context).client.base + "/avatar/${this.username}";
}
}

View File

@ -33,24 +33,21 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
Widget _namespacesWidget() {
List<Widget> namespacesList = <Widget>[];
_namespaces
.asMap()
.forEach((i, namespace) => namespacesList.add(ListTile(
leading: const Icon(Icons.folder),
title: Text(namespace.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
))
);
_namespaces.asMap().forEach((i, namespace) => namespacesList.add(ListTile(
leading: const Icon(Icons.folder),
title: Text(namespace.title),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
return this._loading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
child: ListView(
padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context,
tiles: namespacesList).toList(),
padding: EdgeInsets.zero,
children:
ListTile.divideTiles(context: context, tiles: namespacesList)
.toList(),
),
onRefresh: _loadNamespaces,
);
@ -85,18 +82,18 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
MaterialPageRoute(
builder: (context) => NamespaceEditPage(
namespace: _currentNamespace,
)
)
)
),
)))),
],
),
drawer: Drawer(
child: Column(children: <Widget>[
UserAccountsDrawerHeader(
// Removed until we find a way to disable the user email only for some occasions and not everywhere
accountEmail: currentUser?.email == null ? null : Text(currentUser.email),
accountName: currentUser?.username == null ? null : Text(currentUser.username),
accountEmail:
currentUser?.email == null ? null : Text(currentUser.email),
accountName: currentUser?.username == null
? null
: Text(currentUser.username),
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
@ -105,22 +102,24 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: NetworkImage(currentUser.avatarUrl(context)),
backgroundImage:
NetworkImage(currentUser.avatarUrl(context)),
),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(Theme.of(context).primaryColor, BlendMode.multiply),
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(
Theme.of(context).primaryColor, BlendMode.multiply),
),
),
),
Builder(
builder: (BuildContext context) =>
Expanded(
child: _showUserDetails ? _userDetailsWidget(context) : _namespacesWidget(),
)
),
builder: (BuildContext context) => Expanded(
child: _showUserDetails
? _userDetailsWidget(context)
: _namespacesWidget(),
)),
Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
@ -155,8 +154,8 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
builder: (_) => AddDialog(
onAdd: (name) => _addNamespace(name, context),
decoration: InputDecoration(
labelText: 'Namespace',
hintText: 'eg. Personal Namespace',
labelText: 'Namespace',
hintText: 'eg. Personal Namespace',
),
));
}
@ -171,9 +170,9 @@ class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
content: Text('The namespace was created successfully!'),
));
}).catchError((error) => showDialog(
context: context,
builder: (context) => ErrorDialog(error: error),
));
context: context,
builder: (context) => ErrorDialog(error: error),
));
}
Future<void> _loadNamespaces() {

View File

@ -11,7 +11,6 @@ import 'package:vikunja_app/pages/list/list_edit.dart';
import 'package:vikunja_app/pages/list/task_edit.dart';
import 'package:vikunja_app/stores/list_store.dart';
class ListPage extends StatefulWidget {
final TaskList taskList;
@ -41,63 +40,62 @@ class _ListPageState extends State<ListPage> {
Widget build(BuildContext context) {
final taskState = Provider.of<ListProvider>(context);
return Scaffold(
appBar: AppBar(
title: Text(_list.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
),
)
),
),
],
),
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
? RefreshIndicator(
child: taskState.tasks.length > 0
appBar: AppBar(
title: Text(_list.title),
actions: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListEditPage(
list: _list,
),
)),
),
],
),
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
body: !taskState.isLoading
? RefreshIndicator(
child: taskState.tasks.length > 0
? ListenableProvider.value(
value: taskState,
child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
padding: EdgeInsets.symmetric(vertical: 8.0),
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
if (_loadingTasks.isNotEmpty) {
final loadingTask = _loadingTasks.removeLast();
return _buildLoadingTile(loadingTask);
}
if (_loadingTasks.isNotEmpty) {
final loadingTask = _loadingTasks.removeLast();
return _buildLoadingTile(loadingTask);
}
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
if (taskState.maxPages == _currentPage &&
index == taskState.tasks.length - 1) return null;
// 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 - 1)
return null;
if (index >= taskState.tasks.length &&
_currentPage < taskState.maxPages) {
_currentPage++;
_loadTasksForPage(_currentPage);
}
return index < taskState.tasks.length
? _buildTile(taskState.tasks[index])
: null;
}
),
)
if (index >= taskState.tasks.length &&
_currentPage < taskState.maxPages) {
_currentPage++;
_loadTasksForPage(_currentPage);
}
return index < taskState.tasks.length
? _buildTile(taskState.tasks[index])
: null;
}),
)
: Center(child: Text('This list is empty.')),
onRefresh: _loadList,
onRefresh: _loadList,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _addItemDialog(context), child: Icon(Icons.add)),
),
: Center(child: CircularProgressIndicator()),
floatingActionButton: Builder(
builder: (context) => FloatingActionButton(
onPressed: () => _addItemDialog(context), child: Icon(Icons.add)),
),
);
}
@ -156,8 +154,8 @@ class _ListPageState extends State<ListPage> {
builder: (_) => AddDialog(
onAdd: (title) => _addItem(title, context),
decoration: InputDecoration(
labelText: 'Task Name',
hintText: 'eg. Milk',
labelText: 'Task Name',
hintText: 'eg. Milk',
),
),
);
@ -174,13 +172,17 @@ class _ListPageState extends State<ListPage> {
setState(() => _loadingTasks.add(newTask));
Provider.of<ListProvider>(context, listen: false)
.addTask(
context: context,
newTask: newTask,
listId: _list.id,
).then((_) {
context: context,
newTask: newTask,
listId: _list.id,
)
.then((_) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'),
));
setState(() {
_loadingTasks.remove(newTask);
});
});
}
}
}

View File

@ -106,12 +106,14 @@ class _ListEditPageState extends State<ListEditPage> {
));
}).catchError((err) {
setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessengerState scaffoldState = ScaffoldMessenger.of(context);
scaffoldState.showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: Scaffold.of(context).hideCurrentSnackBar),
onPressed: (() => scaffoldState.hideCurrentSnackBar(
reason: SnackBarClosedReason.action))),
),
);
});

View File

@ -59,7 +59,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
builder: (BuildContext context) => SafeArea(
child: Form(
key: _formKey,
child: ListView(padding: const EdgeInsets.all(16.0), children: <Widget>[
child: ListView(padding: const EdgeInsets.all(16.0), children: <
Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(

View File

@ -58,8 +58,11 @@ class _NamespacePageState extends State<NamespacePage>
color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
_removeList(ls).then((_) => ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text("${ls.title} removed"))));
_removeList(ls).then((_) =>
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(
"${ls.title} removed"))));
},
))).toList(),
)

View File

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

View File

@ -39,13 +39,14 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> addTaskByTitle({BuildContext context, String title, int listId}) {
Future<void> addTaskByTitle(
{BuildContext context, String title, int listId}) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null,
title: title,
createdBy: globalState.currentUser,
done: false,
id: null,
title: title,
createdBy: globalState.currentUser,
done: false,
);
_isLoading = true;
notifyListeners();
@ -69,12 +70,14 @@ class ListProvider with ChangeNotifier {
});
}
Future<void> updateTask({BuildContext context, int id, bool done}) {
void updateTask({BuildContext context, int id, bool done}) {
var globalState = VikunjaGlobal.of(context);
globalState.taskService.update(Task(
globalState.taskService
.update(Task(
id: id,
done: done,
)).then((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.
_tasks.asMap().forEach((i, t) {

View File

@ -30,4 +30,4 @@ const vStandardVerticalPadding = EdgeInsets.symmetric(vertical: 5.0);
const vStandardHorizontalPadding = EdgeInsets.symmetric(horizontal: 5.0);
const vStandardPadding = EdgeInsets.symmetric(horizontal: 5.0, vertical: 5.0);
var vDateFormatLong = DateFormat("EEEE, MMMM d, yyyy 'at' H:mm");
var vDateFormatLong = DateFormat("EEEE, MMMM d, yyyy 'at' H:mm");

View File

@ -2,13 +2,7 @@ import 'package:flutter/material.dart';
import 'package:vikunja_app/theme/constants.dart';
ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light());
ThemeData buildVikunjaDarkTheme() {
ThemeData base = _buildVikunjaTheme(ThemeData.dark());
return base.copyWith(
accentColor: vWhite,
);
}
ThemeData buildVikunjaDarkTheme() => _buildVikunjaTheme(ThemeData.dark());
ThemeData _buildVikunjaTheme(ThemeData base) {
return base.copyWith(

View File

@ -8,4 +8,4 @@ dateTimeFromUnixTimestamp(int timestamp) {
return timestamp == null
? 0
: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
}
}