chore: format code with dart format (#71)

This PR formats all code with dart format and adds a step to the CI so that it will be checked on every push and PR.
This commit is contained in:
Denys Vitali 2024-04-05 22:36:56 +02:00 committed by GitHub
parent 43b9fe6d8f
commit 056b2d72c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
66 changed files with 1559 additions and 1345 deletions

20
.github/workflows/flutter-format.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: Flutter Check Format
on:
pull_request: {}
push: {}
jobs:
release:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v1
- name: Setup Flutter
uses: subosito/flutter-action@v1
with:
channel: stable
- name: Check Dart Format
run: dart format --set-exit-if-changed .

View File

@ -12,15 +12,14 @@ class BucketAPIService extends APIService implements BucketService {
return client return client
.put('/projects/$projectId/buckets', body: bucket.toJSON()) .put('/projects/$projectId/buckets', body: bucket.toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Bucket.fromJSON(response.body); return Bucket.fromJSON(response.body);
}); });
} }
@override @override
Future delete(int projectId, int bucketId) { Future delete(int projectId, int bucketId) {
return client return client.delete('/projects/$projectId/buckets/$bucketId');
.delete('/projects/$projectId/buckets/$bucketId');
} }
/* Not implemented in the Vikunja API /* Not implemented in the Vikunja API
@ -35,13 +34,13 @@ class BucketAPIService extends APIService implements BucketService {
@override @override
Future<Response?> getAllByList(int projectId, Future<Response?> getAllByList(int projectId,
[Map<String, List<String>>? queryParameters]) { [Map<String, List<String>>? queryParameters]) {
return client return client.get('/projects/$projectId/buckets', queryParameters).then(
.get('/projects/$projectId/buckets', queryParameters) (response) => response != null
.then((response) => response != null ? new Response( ? new Response(
convertList(response.body, (result) => Bucket.fromJSON(result)), convertList(response.body, (result) => Bucket.fromJSON(result)),
response.statusCode, response.statusCode,
response.headers response.headers)
) : null); : null);
} }
@override @override
@ -51,10 +50,11 @@ class BucketAPIService extends APIService implements BucketService {
@override @override
Future<Bucket?> update(Bucket bucket) { Future<Bucket?> update(Bucket bucket) {
return client return client
.post('/projects/${bucket.projectId}/buckets/${bucket.id}', body: bucket.toJSON()) .post('/projects/${bucket.projectId}/buckets/${bucket.id}',
body: bucket.toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Bucket.fromJSON(response.body); return Bucket.fromJSON(response.body);
}); });
} }
} }

View File

@ -10,7 +10,6 @@ import 'package:vikunja_app/global.dart';
import '../main.dart'; import '../main.dart';
class Client { class Client {
GlobalKey<ScaffoldMessengerState>? global_scaffold_key; GlobalKey<ScaffoldMessengerState>? global_scaffold_key;
final JsonDecoder _decoder = new JsonDecoder(); final JsonDecoder _decoder = new JsonDecoder();
@ -26,8 +25,6 @@ class Client {
String? post_body; String? post_body;
bool operator ==(dynamic otherClient) { bool operator ==(dynamic otherClient) {
return otherClient._token == _token; return otherClient._token == _token;
} }
@ -40,15 +37,14 @@ class Client {
void reload_ignore_certs(bool? val) { void reload_ignore_certs(bool? val) {
ignoreCertificates = val ?? false; ignoreCertificates = val ?? false;
HttpOverrides.global = new IgnoreCertHttpOverrides(ignoreCertificates); HttpOverrides.global = new IgnoreCertHttpOverrides(ignoreCertificates);
if(global_scaffold_key == null || global_scaffold_key!.currentContext == null) return; if (global_scaffold_key == null ||
VikunjaGlobal global_scaffold_key!.currentContext == null) return;
.of(global_scaffold_key!.currentContext!) VikunjaGlobal.of(global_scaffold_key!.currentContext!)
.settingsManager .settingsManager
.setIgnoreCertificates(ignoreCertificates); .setIgnoreCertificates(ignoreCertificates);
} }
get _headers => get _headers => {
{
'Authorization': _token != '' ? 'Bearer $_token' : '', 'Authorization': _token != '' ? 'Bearer $_token' : '',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'User-Agent': 'Vikunja Mobile App' 'User-Agent': 'Vikunja Mobile App'
@ -60,20 +56,15 @@ class Client {
int get hashCode => _token.hashCode; int get hashCode => _token.hashCode;
void configure({String? token, String? base, bool? authenticated}) { void configure({String? token, String? base, bool? authenticated}) {
if (token != null) if (token != null) _token = token;
_token = token;
if (base != null) { if (base != null) {
base = base.replaceAll(" ", ""); base = base.replaceAll(" ", "");
if(base.endsWith("/")) if (base.endsWith("/")) base = base.substring(0, base.length - 1);
base = base.substring(0,base.length-1);
_base = base.endsWith('/api/v1') ? base : '$base/api/v1'; _base = base.endsWith('/api/v1') ? base : '$base/api/v1';
} }
if (authenticated != null) if (authenticated != null) this.authenticated = authenticated;
this.authenticated = authenticated;
} }
void reset() { void reset() {
_token = _base = ''; _token = _base = '';
authenticated = false; authenticated = false;
@ -85,54 +76,61 @@ class Client {
// why are we doing it like this? because Uri doesnt have setters. wtf. // why are we doing it like this? because Uri doesnt have setters. wtf.
uri = Uri( uri = Uri(
scheme: uri.scheme, scheme: uri.scheme,
userInfo: uri.userInfo, userInfo: uri.userInfo,
host: uri.host, host: uri.host,
port: uri.port, port: uri.port,
path: uri.path, path: uri.path,
//queryParameters: {...uri.queryParameters, ...?queryParameters}, //queryParameters: {...uri.queryParameters, ...?queryParameters},
queryParameters: queryParameters, queryParameters: queryParameters,
fragment: uri.fragment fragment: uri.fragment);
);
return http.get(uri, headers: _headers) return http
.then(_handleResponse).onError((error, stackTrace) => .get(uri, headers: _headers)
_handleError(error, stackTrace)); .then(_handleResponse)
.onError((error, stackTrace) => _handleError(error, stackTrace));
} }
Future<Response?> delete(String url) { Future<Response?> delete(String url) {
return http.delete( return http
'${this.base}$url'.toUri()!, .delete(
headers: _headers, '${this.base}$url'.toUri()!,
).then(_handleResponse).onError((error, stackTrace) => headers: _headers,
_handleError(error, stackTrace)); )
.then(_handleResponse)
.onError((error, stackTrace) => _handleError(error, stackTrace));
} }
Future<Response?> post(String url, {dynamic body}) { Future<Response?> post(String url, {dynamic body}) {
return http.post( return http
'${this.base}$url'.toUri()!, .post(
headers: _headers, '${this.base}$url'.toUri()!,
body: _encoder.convert(body), headers: _headers,
) body: _encoder.convert(body),
.then(_handleResponse).onError((error, stackTrace) => )
_handleError(error, stackTrace)); .then(_handleResponse)
.onError((error, stackTrace) => _handleError(error, stackTrace));
} }
Future<Response?> put(String url, {dynamic body}) { Future<Response?> put(String url, {dynamic body}) {
return http.put( return http
'${this.base}$url'.toUri()!, .put(
headers: _headers, '${this.base}$url'.toUri()!,
body: _encoder.convert(body), headers: _headers,
) body: _encoder.convert(body),
.then(_handleResponse).onError((error, stackTrace) => )
_handleError(error, stackTrace)); .then(_handleResponse)
.onError((error, stackTrace) => _handleError(error, stackTrace));
} }
Response? _handleError(Object? e, StackTrace? st) { Response? _handleError(Object? e, StackTrace? st) {
if(global_scaffold_key == null) return null; if (global_scaffold_key == null) return null;
SnackBar snackBar = SnackBar( SnackBar snackBar = SnackBar(
content: Text("Error on request: " + e.toString()), content: Text("Error on request: " + e.toString()),
action: SnackBarAction(label: "Clear", onPressed: () => global_scaffold_key!.currentState?.clearSnackBars()),); action: SnackBarAction(
label: "Clear",
onPressed: () => global_scaffold_key!.currentState?.clearSnackBars()),
);
global_scaffold_key!.currentState?.showSnackBar(snackBar); global_scaffold_key!.currentState?.showSnackBar(snackBar);
return null; return null;
} }
@ -145,39 +143,38 @@ class Client {
return map; return map;
} }
Error? _handleResponseErrors(http.Response response) { Error? _handleResponseErrors(http.Response response) {
if (response.statusCode < 200 || if (response.statusCode < 200 || response.statusCode >= 400) {
response.statusCode >= 400) {
Map<String, dynamic> error; Map<String, dynamic> error;
error = _decoder.convert(response.body); error = _decoder.convert(response.body);
final SnackBar snackBar = SnackBar( final SnackBar snackBar = SnackBar(
content: Text( content:
"Error code " + response.statusCode.toString() + " received."), Text("Error code " + response.statusCode.toString() + " received."),
action: globalNavigatorKey.currentContext == null ? null : SnackBarAction( action: globalNavigatorKey.currentContext == null
label: ("Details"), ? null
onPressed: () { : SnackBarAction(
showDialog( label: ("Details"),
context: globalNavigatorKey.currentContext!, onPressed: () {
builder: (BuildContext context) => showDialog(
AlertDialog( context: globalNavigatorKey.currentContext!,
title: Text("Error ${response.statusCode}"), builder: (BuildContext context) => AlertDialog(
content: Column( title: Text("Error ${response.statusCode}"),
crossAxisAlignment: CrossAxisAlignment.start, content: Column(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start,
children: [ mainAxisSize: MainAxisSize.min,
Text("Message: ${error["message"]}", textAlign: TextAlign.start,), children: [
Text("Url: ${response.request!.url.toString()}"), Text(
], "Message: ${error["message"]}",
) textAlign: TextAlign.start,
) ),
); Text("Url: ${response.request!.url.toString()}"),
}, ],
), )));
},
),
); );
if(global_scaffold_key != null && showSnackBar) if (global_scaffold_key != null && showSnackBar)
global_scaffold_key!.currentState?.showSnackBar(snackBar); global_scaffold_key!.currentState?.showSnackBar(snackBar);
else else
print("error on request: ${error["message"]}"); print("error on request: ${error["message"]}");

View File

@ -14,7 +14,7 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Label.fromJson(response.body); return Label.fromJson(response.body);
}); });
} }
@override @override
@ -22,9 +22,9 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
return client return client
.delete('/tasks/${lt.task!.id}/labels/${lt.label.id}') .delete('/tasks/${lt.task!.id}/labels/${lt.label.id}')
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Label.fromJson(response.body); return Label.fromJson(response.body);
}); });
} }
@override @override
@ -32,10 +32,9 @@ class LabelTaskAPIService extends APIService implements LabelTaskService {
String? params = String? params =
query == null ? null : '?s=' + Uri.encodeQueryComponent(query); 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) {
(label) { if (label == null) return null;
if (label == null) return null; return convertList(label, (result) => Label.fromJson(result));
return convertList(label, (result) => Label.fromJson(result)); });
});
} }
} }

View File

@ -11,15 +11,14 @@ class LabelTaskBulkAPIService extends APIService
@override @override
Future<List<Label>?> update(Task task, List<Label>? labels) { Future<List<Label>?> update(Task task, List<Label>? labels) {
if(labels == null) if (labels == null) labels = [];
labels = [];
return client return client
.post('/tasks/${task.id}/labels/bulk', .post('/tasks/${task.id}/labels/bulk',
body: LabelTaskBulk(labels: labels).toJSON()) body: LabelTaskBulk(labels: labels).toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return convertList( return convertList(
response.body['labels'], (result) => Label.fromJson(result)); response.body['labels'], (result) => Label.fromJson(result));
}); });
} }
} }

View File

@ -8,52 +8,43 @@ class LabelAPIService extends APIService implements LabelService {
@override @override
Future<Label?> create(Label label) { Future<Label?> create(Label label) {
return client return client.put('/labels', body: label.toJSON()).then((response) {
.put('/labels', body: label.toJSON()) if (response == null) return null;
.then((response) { return Label.fromJson(response.body);
if (response == null) return null; });
return Label.fromJson(response.body);
});
} }
@override @override
Future<Label?> delete(Label label) { Future<Label?> delete(Label label) {
return client return client.delete('/labels/${label.id}').then((response) {
.delete('/labels/${label.id}') if (response == null) return null;
.then((response) { return Label.fromJson(response.body);
if (response == null) return null; });
return Label.fromJson(response.body);
});
} }
@override @override
Future<Label?> get(int labelID) { Future<Label?> get(int labelID) {
return client return client.get('/labels/$labelID').then((response) {
.get('/labels/$labelID') if (response == null) return null;
.then((response) { return Label.fromJson(response.body);
if (response == null) return null; });
return Label.fromJson(response.body);
});
} }
@override @override
Future<List<Label>?> getAll({String? query}) { Future<List<Label>?> getAll({String? query}) {
String params = String params =
query == null ? '' : '?s=' + Uri.encodeQueryComponent(query); query == null ? '' : '?s=' + Uri.encodeQueryComponent(query);
return client.get('/labels$params').then( return client.get('/labels$params').then((response) {
(response) { if (response == null) return null;
if (response == null) return null; return convertList(response.body, (result) => Label.fromJson(result));
return convertList(response.body, (result) => Label.fromJson(result)); });
});
} }
@override @override
Future<Label?> update(Label label) { Future<Label?> update(Label label) {
return client return client.post('/labels/${label.id}', body: label).then((response) {
.post('/labels/${label.id}', body: label) if (response == null) return null;
.then((response) { return Label.fromJson(response.body);
if (response == null) return null; });
return Label.fromJson(response.body);
});
} }
} }

View File

@ -8,7 +8,9 @@ import 'package:vikunja_app/service/services.dart';
class ListAPIService extends APIService implements ListService { class ListAPIService extends APIService implements ListService {
FlutterSecureStorage _storage; FlutterSecureStorage _storage;
ListAPIService(Client client, FlutterSecureStorage storage) : _storage = storage, super(client); ListAPIService(Client client, FlutterSecureStorage storage)
: _storage = storage,
super(client);
@override @override
Future<TaskList?> create(namespaceId, TaskList tl) { Future<TaskList?> create(namespaceId, TaskList tl) {
@ -16,9 +18,9 @@ class ListAPIService extends APIService implements ListService {
return client return client
.put('/namespaces/$namespaceId/lists', body: tl.toJSON()) .put('/namespaces/$namespaceId/lists', body: tl.toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return TaskList.fromJson(response.body); return TaskList.fromJson(response.body);
}); });
} }
@override @override
@ -32,12 +34,10 @@ class ListAPIService extends APIService implements ListService {
if (response == null) return null; if (response == null) return null;
final map = response.body; final map = response.body;
if (map.containsKey('id')) { if (map.containsKey('id')) {
return client return client.get("/lists/$listId/tasks").then((tasks) {
.get("/lists/$listId/tasks") map['tasks'] = tasks?.body;
.then((tasks) { return TaskList.fromJson(map);
map['tasks'] = tasks?.body; });
return TaskList.fromJson(map);
});
} }
return TaskList.fromJson(map); return TaskList.fromJson(map);
}); });
@ -45,47 +45,44 @@ class ListAPIService extends APIService implements ListService {
@override @override
Future<List<TaskList>?> getAll() { Future<List<TaskList>?> getAll() {
return client.get('/lists').then( return client.get('/lists').then((list) {
(list) { if (list == null || list.statusCode != 200) return null;
if (list == null || list.statusCode != 200) return null; if (list.body.toString().isEmpty) return Future.value([]);
if (list.body.toString().isEmpty) print(list.statusCode);
return Future.value([]); return convertList(list.body, (result) => TaskList.fromJson(result));
print(list.statusCode); });
return convertList(list.body, (result) => TaskList.fromJson(result));});
} }
@override @override
Future<List<TaskList>?> getByNamespace(int namespaceId) { Future<List<TaskList>?> getByNamespace(int namespaceId) {
// TODO there needs to be a better way for this. /namespaces/-2/lists should // TODO there needs to be a better way for this. /namespaces/-2/lists should
// return favorite lists // return favorite lists
if(namespaceId == -2) { if (namespaceId == -2) {
// Favourites. // Favourites.
return getAll().then((value) { return getAll().then((value) {
if (value == null) return null; if (value == null) return null;
value.removeWhere((element) => !element.isFavorite); return value;}); value.removeWhere((element) => !element.isFavorite);
return value;
});
} }
return client.get('/namespaces/$namespaceId/lists').then( return client.get('/namespaces/$namespaceId/lists').then((list) {
(list) { if (list == null || list.statusCode != 200) return null;
if (list == null || list.statusCode != 200) return null; return convertList(list.body, (result) => TaskList.fromJson(result));
return convertList(list.body, (result) => TaskList.fromJson(result)); });
});
} }
@override @override
Future<TaskList?> update(TaskList tl) { Future<TaskList?> update(TaskList tl) {
return client return client.post('/lists/${tl.id}', body: tl.toJSON()).then((response) {
.post('/lists/${tl.id}', body: tl.toJSON()) if (response == null) return null;
.then((response) { return TaskList.fromJson(response.body);
if (response == null) return null; });
return TaskList.fromJson(response.body);
});
} }
@override @override
Future<String> getDisplayDoneTasks(int listId) { Future<String> getDisplayDoneTasks(int listId) {
return _storage.read(key: "display_done_tasks_list_$listId").then((value) return _storage.read(key: "display_done_tasks_list_$listId").then((value) {
{ if (value == null) {
if(value == null) {
// TODO: implement default value // TODO: implement default value
setDisplayDoneTasks(listId, "1"); setDisplayDoneTasks(listId, "1");
return Future.value("1"); return Future.value("1");

View File

@ -42,8 +42,8 @@ class NamespaceAPIService extends APIService implements NamespaceService {
return client return client
.post('/namespaces/${ns.id}', body: ns.toJSON()) .post('/namespaces/${ns.id}', body: ns.toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Namespace.fromJson(response.body); return Namespace.fromJson(response.body);
}); });
} }
} }

View File

@ -6,7 +6,9 @@ import 'package:vikunja_app/service/services.dart';
class ProjectAPIService extends APIService implements ProjectService { class ProjectAPIService extends APIService implements ProjectService {
FlutterSecureStorage _storage; FlutterSecureStorage _storage;
ProjectAPIService(client, storage) : _storage = storage, super(client); ProjectAPIService(client, storage)
: _storage = storage,
super(client);
@override @override
Future<Project?> create(Project p) { Future<Project?> create(Project p) {
@ -47,9 +49,7 @@ class ProjectAPIService extends APIService implements ProjectService {
@override @override
Future<Project?> update(Project p) { Future<Project?> update(Project p) {
return client return client.post('/projects/${p.id}', body: p.toJSON()).then((response) {
.post('/projects/${p.id}', body: p.toJSON())
.then((response) {
if (response == null) return null; if (response == null) return null;
return Project.fromJson(response.body); return Project.fromJson(response.body);
}); });
@ -57,9 +57,8 @@ class ProjectAPIService extends APIService implements ProjectService {
@override @override
Future<String> getDisplayDoneTasks(int listId) { Future<String> getDisplayDoneTasks(int listId) {
return _storage.read(key: "display_done_tasks_list_$listId").then((value) return _storage.read(key: "display_done_tasks_list_$listId").then((value) {
{ if (value == null) {
if(value == null) {
// TODO: implement default value // TODO: implement default value
setDisplayDoneTasks(listId, "1"); setDisplayDoneTasks(listId, "1");
return Future.value("1"); return Future.value("1");
@ -82,5 +81,4 @@ class ProjectAPIService extends APIService implements ProjectService {
void setDefaultList(int? listId) { void setDefaultList(int? listId) {
_storage.write(key: "default_list_id", value: listId.toString()); _storage.write(key: "default_list_id", value: listId.toString());
} }
}
}

View File

@ -10,8 +10,7 @@ class ServerAPIService extends APIService implements ServerService {
@override @override
Future<Server?> getInfo() { Future<Server?> getInfo() {
return client.get('/info').then((value) { return client.get('/info').then((value) {
if(value == null) if (value == null) return null;
return null;
return Server.fromJson(value.body); return Server.fromJson(value.body);
}); });
} }

View File

@ -15,25 +15,22 @@ class TaskAPIService extends APIService implements TaskService {
return client return client
.put('/projects/$projectId/tasks', body: task.toJSON()) .put('/projects/$projectId/tasks', body: task.toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Task.fromJson(response.body); return Task.fromJson(response.body);
}); });
} }
@override @override
Future<Task?> get(int listId) { Future<Task?> get(int listId) {
return client return client.get('/project/$listId/tasks').then((response) {
.get('/project/$listId/tasks') if (response == null) return null;
.then((response) { return Task.fromJson(response.body);
if (response == null) return null; });
return Task.fromJson(response.body);
});
} }
@override @override
Future delete(int taskId) { Future delete(int taskId) {
return client return client.delete('/tasks/$taskId');
.delete('/tasks/$taskId');
} }
@override @override
@ -41,36 +38,38 @@ class TaskAPIService extends APIService implements TaskService {
return client return client
.post('/tasks/${task.id}', body: task.toJSON()) .post('/tasks/${task.id}', body: task.toJSON())
.then((response) { .then((response) {
if (response == null) return null; if (response == null) return null;
return Task.fromJson(response.body); return Task.fromJson(response.body);
}); });
} }
@override @override
Future<List<Task>?> getAll() { Future<List<Task>?> getAll() {
return client return client.get('/tasks/all').then((response) {
.get('/tasks/all') int page_n = 0;
.then((response) { if (response == null) return null;
int page_n = 0; if (response.headers["x-pagination-total-pages"] != null) {
if (response == null) return null; page_n = int.parse(response.headers["x-pagination-total-pages"]!);
if (response.headers["x-pagination-total-pages"] != null) { } else {
page_n = int.parse(response.headers["x-pagination-total-pages"]!); return Future.value(response.body);
} else { }
return Future.value(response.body);
}
List<Future<void>> futureList = []; List<Future<void>> futureList = [];
List<Task> taskList = []; List<Task> taskList = [];
for(int i = 0; i < page_n; i++) { for (int i = 0; i < page_n; i++) {
Map<String, List<String>> paramMap = { Map<String, List<String>> paramMap = {
"page": [i.toString()] "page": [i.toString()]
}; };
futureList.add(client.get('/tasks/all', paramMap).then((pageResponse) {convertList(pageResponse?.body, (result) {taskList.add(Task.fromJson(result));});})); futureList.add(client.get('/tasks/all', paramMap).then((pageResponse) {
} convertList(pageResponse?.body, (result) {
return Future.wait(futureList).then((value) { taskList.add(Task.fromJson(result));
return taskList;
}); });
}));
}
return Future.wait(futureList).then((value) {
return taskList;
});
}); });
} }
@ -78,14 +77,15 @@ class TaskAPIService extends APIService implements TaskService {
Future<Response?> getAllByProject(int projectId, Future<Response?> getAllByProject(int projectId,
[Map<String, List<String>>? queryParameters]) { [Map<String, List<String>>? queryParameters]) {
return client return client
.get('/projects/$projectId/tasks', queryParameters).then( .get('/projects/$projectId/tasks', queryParameters)
(response) { .then((response) {
return response != null ? return response != null
new Response( ? new Response(
convertList(response.body, (result) => Task.fromJson(result)), convertList(response.body, (result) => Task.fromJson(result)),
response.statusCode, response.statusCode,
response.headers) : null; response.headers)
}); : null;
});
} }
@override @override
@ -94,16 +94,13 @@ class TaskAPIService extends APIService implements TaskService {
//optionString = "?sort_by[]=due_date&sort_by[]=id&order_by[]=asc&order_by[]=desc&filter_by[]=done&filter_value[]=false&filter_comparator[]=equals&filter_concat=and&filter_include_nulls=false&page=1"; //optionString = "?sort_by[]=due_date&sort_by[]=id&order_by[]=asc&order_by[]=desc&filter_by[]=done&filter_value[]=false&filter_comparator[]=equals&filter_concat=and&filter_include_nulls=false&page=1";
//print(optionString); //print(optionString);
return client return client.get('/tasks/all', optionsMap).then((response) {
.get('/tasks/all', optionsMap) if (response == null) return null;
.then((response) { return convertList(response.body, (result) => Task.fromJson(result));
if (response == null) return null;
return convertList(response.body, (result) => Task.fromJson(result));
}); });
} }
@override @override
// TODO: implement maxPages // TODO: implement maxPages
int get maxPages => maxPages; int get maxPages => maxPages;
} }

View File

@ -9,21 +9,23 @@ class UserAPIService extends APIService implements UserService {
UserAPIService(Client client) : super(client); UserAPIService(Client client) : super(client);
@override @override
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String? totp}) async { Future<UserTokenPair> login(String username, password,
{bool rememberMe = false, String? totp}) async {
var body = { var body = {
'long_token': rememberMe, 'long_token': rememberMe,
'password': password, 'password': password,
'username': username, 'username': username,
}; };
if(totp != null) { if (totp != null) {
body['totp_passcode'] = totp; body['totp_passcode'] = totp;
} }
var response = await client.post('/login', body: body); var response = await client.post('/login', body: body);
var token = response?.body["token"]; var token = response?.body["token"];
if(token == null || response == null || response.error != null) if (token == null || response == null || response.error != null)
return Future.value(UserTokenPair(null, null, return Future.value(UserTokenPair(null, null,
error: response != null ? response.body["code"] : 0, error: response != null ? response.body["code"] : 0,
errorString: response != null ? response.body["message"] : "Login error")); errorString:
response != null ? response.body["message"] : "Login error"));
client.configure(token: token); client.configure(token: token);
return UserAPIService(client) return UserAPIService(client)
.getCurrentUser() .getCurrentUser()
@ -46,9 +48,12 @@ class UserAPIService extends APIService implements UserService {
} }
@override @override
Future<UserSettings?> setCurrentUserSettings(UserSettings userSettings) async { Future<UserSettings?> setCurrentUserSettings(
return client.post('/user/settings/general', body: userSettings.toJson()).then((response) { UserSettings userSettings) async {
if(response == null) return null; return client
.post('/user/settings/general', body: userSettings.toJson())
.then((response) {
if (response == null) return null;
return userSettings; return userSettings;
}); });
} }

View File

@ -5,7 +5,6 @@ import 'dart:convert';
import 'package:http/http.dart'; import 'package:http/http.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class VersionChecker { class VersionChecker {
GlobalKey<ScaffoldMessengerState> snackbarKey; GlobalKey<ScaffoldMessengerState> snackbarKey;
VersionChecker(this.snackbarKey); VersionChecker(this.snackbarKey);
@ -47,7 +46,8 @@ class VersionChecker {
content: Text("New version available: $latest"), content: Text("New version available: $latest"),
action: SnackBarAction( action: SnackBarAction(
label: "View on Github", label: "View on Github",
onPressed: () => launchUrl(Uri.parse(repo), mode: LaunchMode.externalApplication)), onPressed: () => launchUrl(Uri.parse(repo),
mode: LaunchMode.externalApplication)),
); );
snackbarKey.currentState?.showSnackBar(snackBar); snackbarKey.currentState?.showSnackBar(snackBar);
} }

View File

@ -4,7 +4,8 @@ import 'package:vikunja_app/global.dart';
import 'dart:developer'; import 'dart:developer';
import '../models/task.dart'; import '../models/task.dart';
enum NewTaskDue {day,week, month, custom} enum NewTaskDue { day, week, month, custom }
// TODO: add to enum above // TODO: add to enum above
Map<NewTaskDue, Duration> newTaskDueToDuration = { Map<NewTaskDue, Duration> newTaskDueToDuration = {
NewTaskDue.day: Duration(days: 1), NewTaskDue.day: Duration(days: 1),
@ -16,11 +17,11 @@ class AddDialog extends StatefulWidget {
final ValueChanged<String>? onAdd; final ValueChanged<String>? onAdd;
final void Function(String title, DateTime? dueDate)? onAddTask; final void Function(String title, DateTime? dueDate)? onAddTask;
final InputDecoration? decoration; final InputDecoration? decoration;
const AddDialog({Key? key, this.onAdd, this.decoration, this.onAddTask}) : super(key: key); const AddDialog({Key? key, this.onAdd, this.decoration, this.onAddTask})
: super(key: key);
@override @override
State<StatefulWidget> createState() => AddDialogState(); State<StatefulWidget> createState() => AddDialogState();
} }
class AddDialogState extends State<AddDialog> { class AddDialogState extends State<AddDialog> {
@ -30,13 +31,11 @@ class AddDialogState extends State<AddDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if(newTaskDue != NewTaskDue.custom) if (newTaskDue != NewTaskDue.custom)
customDueDate = DateTime.now().add(newTaskDueToDuration[newTaskDue]!); customDueDate = DateTime.now().add(newTaskDueToDuration[newTaskDue]!);
return new AlertDialog( return new AlertDialog(
contentPadding: const EdgeInsets.all(16.0), contentPadding: const EdgeInsets.all(16.0),
content: new Column( content: new Column(mainAxisSize: MainAxisSize.min, children: [
mainAxisSize: MainAxisSize.min,
children: [
Row(children: <Widget>[ Row(children: <Widget>[
Expanded( Expanded(
child: new TextField( child: new TextField(
@ -46,14 +45,24 @@ class AddDialogState extends State<AddDialog> {
), ),
), ),
]), ]),
widget.onAddTask != null ? taskDueList("1 Day", NewTaskDue.day) : new Container(), widget.onAddTask != null
widget.onAddTask != null ? taskDueList("1 Week", NewTaskDue.week) : new Container(), ? taskDueList("1 Day", NewTaskDue.day)
widget.onAddTask != null ? taskDueList("1 Month", NewTaskDue.month) : new Container(), : new Container(),
widget.onAddTask != null ? VikunjaDateTimePicker( widget.onAddTask != null
label: "Enter exact time", ? taskDueList("1 Week", NewTaskDue.week)
onChanged: (value) {setState(() => newTaskDue = NewTaskDue.custom); customDueDate = value;}, : new Container(),
widget.onAddTask != null
) : new Container(), ? taskDueList("1 Month", NewTaskDue.month)
: new Container(),
widget.onAddTask != null
? VikunjaDateTimePicker(
label: "Enter exact time",
onChanged: (value) {
setState(() => newTaskDue = NewTaskDue.custom);
customDueDate = value;
},
)
: new Container(),
//],) //],)
]), ]),
actions: <Widget>[ actions: <Widget>[
@ -66,7 +75,7 @@ class AddDialogState extends State<AddDialog> {
onPressed: () { onPressed: () {
if (widget.onAdd != null && textController.text.isNotEmpty) if (widget.onAdd != null && textController.text.isNotEmpty)
widget.onAdd!(textController.text); widget.onAdd!(textController.text);
if(widget.onAddTask != null && textController.text.isNotEmpty) { if (widget.onAddTask != null && textController.text.isNotEmpty) {
widget.onAddTask!(textController.text, customDueDate); widget.onAddTask!(textController.text, customDueDate);
} }
Navigator.pop(context); Navigator.pop(context);
@ -78,9 +87,15 @@ class AddDialogState extends State<AddDialog> {
Widget taskDueList(String name, NewTaskDue thisNewTaskDue) { Widget taskDueList(String name, NewTaskDue thisNewTaskDue) {
return Row(children: [ return Row(children: [
Checkbox(value: newTaskDue == thisNewTaskDue, onChanged: (value) { Checkbox(
newTaskDue = thisNewTaskDue; value: newTaskDue == thisNewTaskDue,
setState(() => customDueDate = DateTime.now().add(newTaskDueToDuration[thisNewTaskDue]!));}, shape: CircleBorder(),), onChanged: (value) {
newTaskDue = thisNewTaskDue;
setState(() => customDueDate =
DateTime.now().add(newTaskDueToDuration[thisNewTaskDue]!));
},
shape: CircleBorder(),
),
Text(name), Text(name),
]); ]);
} }

View File

@ -36,13 +36,15 @@ class _BucketLimitDialogState extends State<BucketLimitDialog> {
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly, FilteringTextInputFormatter.digitsOnly,
], ],
onSubmitted: (text) => Navigator.of(context).pop(int.parse(text)), onSubmitted: (text) =>
Navigator.of(context).pop(int.parse(text)),
), ),
), ),
Column( Column(
children: <Widget>[ children: <Widget>[
IconButton( IconButton(
onPressed: () => _controller.text = '${int.parse(_controller.text) + 1}', onPressed: () =>
_controller.text = '${int.parse(_controller.text) + 1}',
icon: Icon(Icons.expand_less), icon: Icon(Icons.expand_less),
), ),
IconButton( IconButton(
@ -68,7 +70,8 @@ class _BucketLimitDialogState extends State<BucketLimitDialog> {
child: Text('Remove Limit'), child: Text('Remove Limit'),
), ),
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(int.parse(_controller.text)), onPressed: () =>
Navigator.of(context).pop(int.parse(_controller.text)),
child: Text('Done'), child: Text('Done'),
) )
], ],

View File

@ -11,7 +11,7 @@ import 'package:vikunja_app/theme/constants.dart';
import '../stores/project_store.dart'; import '../stores/project_store.dart';
enum DropLocation {above, below, none} enum DropLocation { above, below, none }
class TaskData { class TaskData {
final Task task; final Task task;
@ -37,7 +37,8 @@ class BucketTaskCard extends StatefulWidget {
State<BucketTaskCard> createState() => _BucketTaskCardState(); State<BucketTaskCard> createState() => _BucketTaskCardState();
} }
class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAliveClientMixin { class _BucketTaskCardState extends State<BucketTaskCard>
with AutomaticKeepAliveClientMixin {
Size? _cardSize; Size? _cardSize;
bool _dragging = false; bool _dragging = false;
DropLocation _dropLocation = DropLocation.none; DropLocation _dropLocation = DropLocation.none;
@ -49,7 +50,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
if (_cardSize == null) _updateCardSize(context); if (_cardSize == null) _updateCardSize(context);
final taskState = Provider.of<ProjectProvider>(context); final taskState = Provider.of<ProjectProvider>(context);
final bucket = taskState.buckets[taskState.buckets.indexWhere((b) => b.id == widget.task.bucketId)]; final bucket = taskState.buckets[
taskState.buckets.indexWhere((b) => b.id == widget.task.bucketId)];
// default chip height: 32 // default chip height: 32
const double chipHeight = 28; const double chipHeight = 28;
const chipConstraints = BoxConstraints(maxHeight: chipHeight); const chipConstraints = BoxConstraints(maxHeight: chipHeight);
@ -59,7 +61,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
children: <Widget>[ children: <Widget>[
Text( Text(
widget.task.identifier.isNotEmpty widget.task.identifier.isNotEmpty
? '#${widget.task.identifier}' : '${widget.task.id}', ? '#${widget.task.identifier}'
: '${widget.task.id}',
style: (theme.textTheme.subtitle2 ?? TextStyle()).copyWith( style: (theme.textTheme.subtitle2 ?? TextStyle()).copyWith(
color: Colors.grey, color: Colors.grey,
), ),
@ -67,21 +70,25 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
], ],
); );
if (widget.task.done) { if (widget.task.done) {
identifierRow.children.insert(0, Container( identifierRow.children.insert(
constraints: chipConstraints, 0,
padding: EdgeInsets.only(right: 4), Container(
child: FittedBox( constraints: chipConstraints,
child: Chip( padding: EdgeInsets.only(right: 4),
label: Text('Done'), child: FittedBox(
labelStyle: (theme.textTheme.labelLarge ?? TextStyle()).copyWith( child: Chip(
fontWeight: FontWeight.bold, label: Text('Done'),
color: theme.brightness == Brightness.dark labelStyle:
? Colors.black : Colors.white, (theme.textTheme.labelLarge ?? TextStyle()).copyWith(
fontWeight: FontWeight.bold,
color: theme.brightness == Brightness.dark
? Colors.black
: Colors.white,
),
backgroundColor: vGreen,
),
), ),
backgroundColor: vGreen, ));
),
),
));
} }
final titleRow = Row( final titleRow = Row(
@ -89,9 +96,11 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
Expanded( Expanded(
child: Text( child: Text(
widget.task.title, widget.task.title,
style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16)).copyWith( style: (theme.textTheme.titleMedium ?? TextStyle(fontSize: 16))
.copyWith(
color: theme.brightness == Brightness.dark color: theme.brightness == Brightness.dark
? Colors.white : Colors.black, ? Colors.white
: Colors.black,
), ),
), ),
), ),
@ -145,10 +154,10 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
backgroundColor: Colors.grey, backgroundColor: Colors.grey,
), ),
), ),
label: Text( label: Text((checkboxStatistics.checked == checkboxStatistics.total
(checkboxStatistics.checked == checkboxStatistics.total ? '' : '${checkboxStatistics.checked} of ') ? ''
+ '${checkboxStatistics.total} tasks' : '${checkboxStatistics.checked} of ') +
), '${checkboxStatistics.total} tasks'),
)); ));
} }
if (widget.task.attachments.isNotEmpty) { if (widget.task.attachments.isNotEmpty) {
@ -185,7 +194,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
child: identifierRow, child: identifierRow,
), ),
Padding( Padding(
padding: EdgeInsets.only(top: 4, bottom: labelRow.children.isNotEmpty ? 8 : 0), padding: EdgeInsets.only(
top: 4, bottom: labelRow.children.isNotEmpty ? 8 : 0),
child: Container( child: Container(
constraints: rowConstraints, constraints: rowConstraints,
child: titleRow, child: titleRow,
@ -213,7 +223,9 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
return LongPressDraggable<TaskData>( 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 maxSimultaneousDrags: taskState.taskDragging
? 0
: 1, // only one task can be dragged at a time
onDragStarted: () { onDragStarted: () {
taskState.taskDragging = true; taskState.taskDragging = true;
setState(() => _dragging = true); setState(() => _dragging = true);
@ -223,14 +235,16 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
taskState.taskDragging = false; taskState.taskDragging = false;
setState(() => _dragging = false); setState(() => _dragging = false);
}, },
feedback: (_cardSize == null) ? SizedBox.shrink() : SizedBox.fromSize( feedback: (_cardSize == null)
size: _cardSize, ? SizedBox.shrink()
child: Card( : SizedBox.fromSize(
color: card.color, size: _cardSize,
child: (card.child as InkWell).child, child: Card(
elevation: (card.elevation ?? 0) + 5, color: card.color,
), child: (card.child as InkWell).child,
), elevation: (card.elevation ?? 0) + 5,
),
),
childWhenDragging: SizedBox.shrink(), childWhenDragging: SizedBox.shrink(),
child: () { child: () {
if (_dragging || _cardSize == null) return card; if (_dragging || _cardSize == null) return card;
@ -241,16 +255,19 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
color: Colors.grey, color: Colors.grey,
child: SizedBox.fromSize(size: dropBoxSize), child: SizedBox.fromSize(size: dropBoxSize),
); );
final dropAbove = taskState.taskDragging && _dropLocation == DropLocation.above; final dropAbove =
final dropBelow = taskState.taskDragging && _dropLocation == DropLocation.below; taskState.taskDragging && _dropLocation == DropLocation.above;
final DragTargetLeave<TaskData> dragTargetOnLeave = (data) => setState(() { final dropBelow =
_dropLocation = DropLocation.none; taskState.taskDragging && _dropLocation == DropLocation.below;
_dropData = null; final DragTargetLeave<TaskData> dragTargetOnLeave =
}); (data) => setState(() {
final dragTargetOnWillAccept = (TaskData data, DropLocation dropLocation) { _dropLocation = DropLocation.none;
if (data.task.bucketId != bucket.id) _dropData = null;
if (bucket.limit != 0 && bucket.tasks.length >= bucket.limit) });
return false; final dragTargetOnWillAccept =
(TaskData data, DropLocation dropLocation) {
if (data.task.bucketId != bucket.id) if (bucket.limit != 0 &&
bucket.tasks.length >= bucket.limit) return false;
setState(() { setState(() {
_dropLocation = dropLocation; _dropLocation = dropLocation;
_dropData = data; _dropData = data;
@ -259,7 +276,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
}; };
final DragTargetAccept<TaskData> dragTargetOnAccept = (data) { final DragTargetAccept<TaskData> dragTargetOnAccept = (data) {
final index = bucket.tasks.indexOf(widget.task); final index = bucket.tasks.indexOf(widget.task);
widget.onAccept(data.task, _dropLocation == DropLocation.above ? index : index + 1); widget.onAccept(data.task,
_dropLocation == DropLocation.above ? index : index + 1);
setState(() { setState(() {
_dropLocation = DropLocation.none; _dropLocation = DropLocation.none;
_dropData = null; _dropData = null;
@ -268,7 +286,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
return SizedBox( return SizedBox(
width: cardSize.width, width: cardSize.width,
height: cardSize.height + (dropAbove || dropBelow ? dropBoxSize.height + 4 : 0), height: cardSize.height +
(dropAbove || dropBelow ? dropBoxSize.height + 4 : 0),
child: Stack( child: Stack(
children: <Widget>[ children: <Widget>[
Column( Column(
@ -281,18 +300,22 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
Column( Column(
children: <SizedBox>[ children: <SizedBox>[
SizedBox( SizedBox(
height: (cardSize.height / 2) + (dropAbove ? dropBoxSize.height : 0), height: (cardSize.height / 2) +
(dropAbove ? dropBoxSize.height : 0),
child: DragTarget<TaskData>( child: DragTarget<TaskData>(
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.above), onWillAccept: (data) =>
dragTargetOnWillAccept(data!, DropLocation.above),
onAccept: dragTargetOnAccept, onAccept: dragTargetOnAccept,
onLeave: dragTargetOnLeave, onLeave: dragTargetOnLeave,
builder: (_, __, ___) => SizedBox.expand(), builder: (_, __, ___) => SizedBox.expand(),
), ),
), ),
SizedBox( SizedBox(
height: (cardSize.height / 2) + (dropBelow ? dropBoxSize.height : 0), height: (cardSize.height / 2) +
(dropBelow ? dropBoxSize.height : 0),
child: DragTarget<TaskData>( child: DragTarget<TaskData>(
onWillAccept: (data) => dragTargetOnWillAccept(data!, DropLocation.below), onWillAccept: (data) =>
dragTargetOnWillAccept(data!, DropLocation.below),
onAccept: dragTargetOnAccept, onAccept: dragTargetOnAccept,
onLeave: dragTargetOnLeave, onLeave: dragTargetOnLeave,
builder: (_, __, ___) => SizedBox.expand(), builder: (_, __, ___) => SizedBox.expand(),
@ -309,12 +332,13 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
void _updateCardSize(BuildContext context) { void _updateCardSize(BuildContext context) {
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
if (mounted) setState(() { if (mounted)
_cardSize = context.size; setState(() {
}); _cardSize = context.size;
});
}); });
} }
@override @override
bool get wantKeepAlive => _dragging; bool get wantKeepAlive => _dragging;
} }

View File

@ -26,23 +26,22 @@ class KanbanClass {
Function _onViewTapped, _addItemDialog, notify; Function _onViewTapped, _addItemDialog, notify;
Duration _lastTaskDragUpdateAction = Duration.zero; Duration _lastTaskDragUpdateAction = Duration.zero;
Project _list; Project _list;
Map<int, BucketProps> _bucketProps = {}; Map<int, BucketProps> _bucketProps = {};
KanbanClass(this.context, this.notify, this._onViewTapped,
KanbanClass(this.context, this.notify, this._onViewTapped, this._addItemDialog, this._list) { this._addItemDialog, this._list) {
taskState = Provider.of<ProjectProvider>(context); taskState = Provider.of<ProjectProvider>(context);
} }
Widget kanbanView() { Widget kanbanView() {
final deviceData = MediaQuery.of(context); final deviceData = MediaQuery.of(context);
final portrait = deviceData.orientation == Orientation.portrait; final portrait = deviceData.orientation == Orientation.portrait;
final bucketFraction = portrait ? 0.8 : 0.4; final bucketFraction = portrait ? 0.8 : 0.4;
final bucketWidth = deviceData.size.width * bucketFraction; final bucketWidth = deviceData.size.width * bucketFraction;
if (_pageController == null || _pageController!.viewportFraction != bucketFraction) if (_pageController == null ||
_pageController!.viewportFraction != bucketFraction)
_pageController = PageController(viewportFraction: bucketFraction); _pageController = PageController(viewportFraction: bucketFraction);
print(_list.doneBucketId); print(_list.doneBucketId);
@ -171,14 +170,16 @@ class KanbanClass {
), ),
)); ));
} }
Future<void> _setDoneBucket(BuildContext context, int bucketId) async { Future<void> _setDoneBucket(BuildContext context, int bucketId) async {
//setState(() {}); //setState(() {});
_list = (await VikunjaGlobal.of(context).projectService.update(_list.copyWith(doneBucketId: bucketId)))!; _list = (await VikunjaGlobal.of(context)
.projectService
.update(_list.copyWith(doneBucketId: bucketId)))!;
notify(); notify();
} }
Future<void> _addBucket( Future<void> _addBucket(String title, BuildContext context) async {
String title, BuildContext context) async {
final currentUser = VikunjaGlobal.of(context).currentUser; final currentUser = VikunjaGlobal.of(context).currentUser;
if (currentUser == null) { if (currentUser == null) {
return; return;
@ -257,14 +258,12 @@ class KanbanClass {
SchedulerBinding.instance.addPostFrameCallback((_) { SchedulerBinding.instance.addPostFrameCallback((_) {
if (_bucketProps[bucket.id]!.controller.hasClients) if (_bucketProps[bucket.id]!.controller.hasClients)
//setState(() { //setState(() {
_bucketProps[bucket.id]!.bucketLength = bucket.tasks.length; _bucketProps[bucket.id]!.bucketLength = bucket.tasks.length;
_bucketProps[bucket.id]!.scrollable = _bucketProps[bucket.id]!.scrollable =
_bucketProps[bucket.id]!.controller.position.maxScrollExtent > _bucketProps[bucket.id]!.controller.position.maxScrollExtent > 0;
0; _bucketProps[bucket.id]!.portrait = portrait;
_bucketProps[bucket.id]!.portrait = portrait; //});
//});
notify(); notify();
}); });
if (_bucketProps[bucket.id]!.titleController.text.isEmpty) if (_bucketProps[bucket.id]!.titleController.text.isEmpty)
_bucketProps[bucket.id]!.titleController.text = bucket.title; _bucketProps[bucket.id]!.titleController.text = bucket.title;
@ -428,10 +427,11 @@ class KanbanClass {
final screenSize = MediaQuery.of(context).size; final screenSize = MediaQuery.of(context).size;
const scrollDuration = Duration(milliseconds: 250); const scrollDuration = Duration(milliseconds: 250);
const scrollCurve = Curves.easeInOut; const scrollCurve = Curves.easeInOut;
final updateAction = () { //setState(() => final updateAction = () {
//setState(() =>
_lastTaskDragUpdateAction = details.sourceTimeStamp!; _lastTaskDragUpdateAction = details.sourceTimeStamp!;
notify(); notify();
};//); }; //);
if (details.globalPosition.dx < screenSize.width * 0.1) { if (details.globalPosition.dx < screenSize.width * 0.1) {
// scroll left // scroll left
@ -503,8 +503,8 @@ class KanbanClass {
if (bucket.tasks.length == 0) if (bucket.tasks.length == 0)
DragTarget<TaskData>( DragTarget<TaskData>(
onWillAccept: (data) { onWillAccept: (data) {
/*setState(() =>*/ _bucketProps[bucket.id]!.taskDropSize = /*setState(() =>*/ _bucketProps[bucket.id]!
data?.size;//); .taskDropSize = data?.size; //);
notify(); notify();
return true; return true;
}, },
@ -523,12 +523,12 @@ class KanbanClass {
))); )));
//setState(() => //setState(() =>
_bucketProps[bucket.id]!.taskDropSize = null;//); _bucketProps[bucket.id]!.taskDropSize = null; //);
notify(); notify();
}, },
onLeave: (_) { onLeave: (_) {
//setState(() => //setState(() =>
_bucketProps[bucket.id]!.taskDropSize = null;//) _bucketProps[bucket.id]!.taskDropSize = null; //)
notify(); notify();
}, },
builder: (_, __, ___) => SizedBox.expand(), builder: (_, __, ___) => SizedBox.expand(),
@ -549,12 +549,7 @@ class KanbanClass {
} }
Future<void> loadBucketsForPage(int page) { Future<void> loadBucketsForPage(int page) {
return Provider.of<ProjectProvider>(context, listen: false).loadBuckets( return Provider.of<ProjectProvider>(context, listen: false)
context: context, .loadBuckets(context: context, listId: _list.id, page: page);
listId: _list.id,
page: page
);
} }
} }

View File

@ -20,20 +20,23 @@ class SliverBucketList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate((context, index) { delegate: SliverChildBuilderDelegate((context, index) {
return index >= bucket.tasks.length ? null : BucketTaskCard( return index >= bucket.tasks.length
key: ObjectKey(bucket.tasks[index]), ? null
task: bucket.tasks[index], : BucketTaskCard(
index: index, key: ObjectKey(bucket.tasks[index]),
onDragUpdate: onTaskDragUpdate, task: bucket.tasks[index],
onAccept: (task, index) { index: index,
_moveTaskToBucket(context, task, index); onDragUpdate: onTaskDragUpdate,
}, onAccept: (task, index) {
); _moveTaskToBucket(context, task, index);
},
);
}), }),
); );
} }
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) async { Future<void> _moveTaskToBucket(
BuildContext context, Task task, int index) async {
await Provider.of<ProjectProvider>(context, listen: false).moveTaskToBucket( await Provider.of<ProjectProvider>(context, listen: false).moveTaskToBucket(
context: context, context: context,
task: task, task: task,
@ -42,7 +45,8 @@ class SliverBucketList extends StatelessWidget {
); );
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('\'${task.title}\' was moved to \'${bucket.title}\' successfully!'), content: Text(
'\'${task.title}\' was moved to \'${bucket.title}\' successfully!'),
)); ));
} }
} }

View File

@ -16,12 +16,14 @@ class SliverBucketPersistentHeader extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SliverPersistentHeader( return SliverPersistentHeader(
pinned: true, pinned: true,
delegate: _SliverBucketPersistentHeaderDelegate(child, minExtent, maxExtent), delegate:
_SliverBucketPersistentHeaderDelegate(child, minExtent, maxExtent),
); );
} }
} }
class _SliverBucketPersistentHeaderDelegate extends SliverPersistentHeaderDelegate { class _SliverBucketPersistentHeaderDelegate
extends SliverPersistentHeaderDelegate {
final Widget child; final Widget child;
final double min; final double min;
final double max; final double max;
@ -29,7 +31,8 @@ class _SliverBucketPersistentHeaderDelegate extends SliverPersistentHeaderDelega
_SliverBucketPersistentHeaderDelegate(this.child, this.min, this.max); _SliverBucketPersistentHeaderDelegate(this.child, this.min, this.max);
@override @override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child; return child;
} }
@ -40,8 +43,10 @@ class _SliverBucketPersistentHeaderDelegate extends SliverPersistentHeaderDelega
double get minExtent => min; double get minExtent => min;
@override @override
bool shouldRebuild(covariant _SliverBucketPersistentHeaderDelegate oldDelegate) { bool shouldRebuild(
return oldDelegate.child != child || oldDelegate.min != min || oldDelegate.max != max; covariant _SliverBucketPersistentHeaderDelegate oldDelegate) {
return oldDelegate.child != child ||
oldDelegate.min != min ||
oldDelegate.max != max;
} }
} }

View File

@ -49,95 +49,109 @@ class TaskBottomSheetState extends State<TaskBottomSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
ThemeData theme = Theme.of(context); ThemeData theme = Theme.of(context);
return Container( return Container(
height: MediaQuery.of(context).size.height * 0.9, height: MediaQuery.of(context).size.height * 0.9,
child: Padding( child: Padding(
padding: EdgeInsets.fromLTRB(20, 10, 10, 20), padding: EdgeInsets.fromLTRB(20, 10, 10, 20),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
children: <Widget>[ Row(
Row( // Title and edit button
// Title and edit button mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
children: [ Text(_currentTask.title,
Text(_currentTask.title, style: theme.textTheme.headlineLarge), style: theme.textTheme.headlineLarge),
IconButton(onPressed: () { IconButton(
Navigator.push<Task>( onPressed: () {
context, Navigator.push<Task>(
MaterialPageRoute( context,
builder: (buildContext) => TaskEditPage( MaterialPageRoute(
task: _currentTask, builder: (buildContext) => TaskEditPage(
taskState: widget.taskState, task: _currentTask,
), taskState: widget.taskState,
), ),
) ),
.then((task) => setState(() { )
if (task != null) _currentTask = task; .then((task) => setState(() {
})) if (task != null) _currentTask = task;
.whenComplete(() => widget.onEdit()); }))
}, icon: Icon(Icons.edit)), .whenComplete(() => widget.onEdit());
], },
), icon: Icon(Icons.edit)),
Wrap( ],
spacing: 10,
children: _currentTask.labels.map((Label label) {
return LabelComponent(
label: label,
);
}).toList()),
// description with html rendering
Text("Description", style: theme.textTheme.headlineSmall),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
child: HtmlWidget(_currentTask.description.isNotEmpty ? _currentTask.description : "No description"),
), ),
// Due date Wrap(
Row( spacing: 10,
children: [ children: _currentTask.labels.map((Label label) {
Icon(Icons.access_time), return LabelComponent(
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)), label: label,
Text(_currentTask.dueDate != null ? vDateFormatShort.format(_currentTask.dueDate!.toLocal()) : "No due date"), );
], }).toList()),
),
// start date
Row(
children: [
Icon(Icons.play_arrow_rounded),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.startDate != null ? vDateFormatShort.format(_currentTask.startDate!.toLocal()) : "No start date"),
],
),
// end date
Row(
children: [
Icon(Icons.stop_rounded),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.endDate != null ? vDateFormatShort.format(_currentTask.endDate!.toLocal()) : "No end date"),
],
),
// priority
Row(
children: [
Icon(Icons.priority_high),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.priority != null ? priorityToString(_currentTask.priority) : "No priority"),
],
),
// progress
Row(
children: [
Icon(Icons.percent),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.percent_done != null ? (_currentTask.percent_done! * 100).toInt().toString() + "%" : "Unset"),
],
),
],
),
)
); // description with html rendering
Text("Description", style: theme.textTheme.headlineSmall),
Padding(
padding: EdgeInsets.fromLTRB(10, 0, 0, 0),
child: HtmlWidget(_currentTask.description.isNotEmpty
? _currentTask.description
: "No description"),
),
// Due date
Row(
children: [
Icon(Icons.access_time),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.dueDate != null
? vDateFormatShort.format(_currentTask.dueDate!.toLocal())
: "No due date"),
],
),
// start date
Row(
children: [
Icon(Icons.play_arrow_rounded),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.startDate != null
? vDateFormatShort
.format(_currentTask.startDate!.toLocal())
: "No start date"),
],
),
// end date
Row(
children: [
Icon(Icons.stop_rounded),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.endDate != null
? vDateFormatShort.format(_currentTask.endDate!.toLocal())
: "No end date"),
],
),
// priority
Row(
children: [
Icon(Icons.priority_high),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.priority != null
? priorityToString(_currentTask.priority)
: "No priority"),
],
),
// progress
Row(
children: [
Icon(Icons.percent),
Padding(padding: EdgeInsets.fromLTRB(10, 0, 0, 0)),
Text(_currentTask.percent_done != null
? (_currentTask.percent_done! * 100).toInt().toString() +
"%"
: "Unset"),
],
),
],
),
));
} }
}
}

View File

@ -110,7 +110,10 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
showModalBottomSheet<void>( showModalBottomSheet<void>(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return TaskBottomSheet(task: widget.task, onEdit: widget.onEdit, taskState: taskState); return TaskBottomSheet(
task: widget.task,
onEdit: widget.onEdit,
taskState: taskState);
}); });
}, },
title: widget.showInfo title: widget.showInfo

View File

@ -0,0 +1 @@

View File

@ -40,8 +40,7 @@ class VikunjaDateTimePicker extends StatelessWidget {
onSaved: onSaved, onSaved: onSaved,
onChanged: onChanged, onChanged: onChanged,
onShowPicker: (context, currentValue) { onShowPicker: (context, currentValue) {
if(currentValue == null) if (currentValue == null) currentValue = DateTime.now();
currentValue = DateTime.now();
return _showDatePickerFuture(context, currentValue); return _showDatePickerFuture(context, currentValue);
}, },
); );
@ -51,26 +50,22 @@ class VikunjaDateTimePicker extends StatelessWidget {
return showDialog( return showDialog(
context: context, context: context,
builder: (_) => DatePickerDialog( builder: (_) => DatePickerDialog(
initialDate: currentValue.year <= 1 initialDate:
? DateTime.now() currentValue.year <= 1 ? DateTime.now() : currentValue,
: currentValue, firstDate: DateTime(1900),
firstDate: DateTime(1900), lastDate: DateTime(2100),
lastDate: DateTime(2100), initialCalendarMode: DatePickerMode.day,
initialCalendarMode: DatePickerMode.day, )).then((date) {
)).then((date) { if (date == null) return null;
if(date == null) return showDialog(
return null;
return showDialog(
context: context, context: context,
builder: (_) => builder: (_) => TimePickerDialog(
TimePickerDialog(
initialTime: TimeOfDay.fromDateTime(currentValue), initialTime: TimeOfDay.fromDateTime(currentValue),
) )).then((time) {
).then((time) { if (time == null) return null;
if(time == null) return DateTime(
return null; date.year, date.month, date.day, time.hour, time.minute);
return DateTime(date.year,date.month, date.day,time.hour,time.minute); });
});
}); });
} }
} }

View File

@ -23,7 +23,6 @@ import 'package:workmanager/workmanager.dart';
import 'api/project.dart'; import 'api/project.dart';
import 'main.dart'; import 'main.dart';
class VikunjaGlobal extends StatefulWidget { class VikunjaGlobal extends StatefulWidget {
final Widget child; final Widget child;
final Widget login; final Widget login;
@ -50,7 +49,6 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
UserService? _newUserService; UserService? _newUserService;
NotificationClass _notificationClass = NotificationClass(); NotificationClass _notificationClass = NotificationClass();
User? get currentUser => _currentUser; User? get currentUser => _currentUser;
Client get client => _client; Client get client => _client;
@ -81,7 +79,6 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
NotificationClass get notifications => _notificationClass; NotificationClass get notifications => _notificationClass;
LabelService get labelService => new LabelAPIService(client); LabelService get labelService => new LabelAPIService(client);
LabelTaskService get labelTaskService => new LabelTaskAPIService(client); LabelTaskService get labelTaskService => new LabelTaskAPIService(client);
@ -89,21 +86,25 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
LabelTaskBulkAPIService get labelTaskBulkService => LabelTaskBulkAPIService get labelTaskBulkService =>
new LabelTaskBulkAPIService(client); new LabelTaskBulkAPIService(client);
late String currentTimeZone; late String currentTimeZone;
void updateWorkmanagerDuration() { void updateWorkmanagerDuration() {
Workmanager().cancelAll().then((value) { Workmanager().cancelAll().then((value) {
settingsManager.getWorkmanagerDuration().then((duration) settingsManager.getWorkmanagerDuration().then((duration) {
{ if (duration.inMinutes > 0) {
if(duration.inMinutes > 0) { Workmanager().registerPeriodicTask("update-tasks", "update-tasks",
Workmanager().registerPeriodicTask( frequency: duration,
"update-tasks", "update-tasks", frequency: duration, constraints: Constraints(networkType: NetworkType.connected), constraints: Constraints(networkType: NetworkType.connected),
initialDelay: Duration(seconds: 15), inputData: {"client_token": client.token, "client_base": client.base}); initialDelay: Duration(seconds: 15),
inputData: {
"client_token": client.token,
"client_base": client.base
});
} }
Workmanager().registerPeriodicTask( Workmanager().registerPeriodicTask("refresh-token", "refresh-token",
"refresh-token", "refresh-token", frequency: Duration(hours: 12), constraints: Constraints(networkType: NetworkType.connected), frequency: Duration(hours: 12),
constraints: Constraints(networkType: NetworkType.connected),
initialDelay: Duration(seconds: 15)); initialDelay: Duration(seconds: 15));
}); });
}); });
@ -113,13 +114,15 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
void initState() { void initState() {
super.initState(); super.initState();
_client = Client(snackbarKey); _client = Client(snackbarKey);
settingsManager.getIgnoreCertificates().then((value) => client.reload_ignore_certs(value == "1")); settingsManager
.getIgnoreCertificates()
.then((value) => client.reload_ignore_certs(value == "1"));
_newUserService = UserAPIService(client); _newUserService = UserAPIService(client);
_loadCurrentUser(); _loadCurrentUser();
tz.initializeTimeZones(); tz.initializeTimeZones();
notifications.notificationInitializer(); notifications.notificationInitializer();
settingsManager.getVersionNotifications().then((value) { settingsManager.getVersionNotifications().then((value) {
if(value == "1") { if (value == "1") {
versionChecker.postVersionCheckSnackbar(); versionChecker.postVersionCheckSnackbar();
} }
}); });
@ -152,17 +155,16 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
}); });
} }
void logoutUser(BuildContext context) async { void logoutUser(BuildContext context) async {
// _storage.deleteAll().then((_) { // _storage.deleteAll().then((_) {
var userId = await _storage.read(key: "currentUser"); var userId = await _storage.read(key: "currentUser");
await _storage.delete(key: userId!); //delete token await _storage.delete(key: userId!); //delete token
await _storage.delete(key: "${userId}_base"); await _storage.delete(key: "${userId}_base");
setState(() { setState(() {
client.reset(); client.reset();
_currentUser = null; _currentUser = null;
}); });
/* }).catchError((err) { /* }).catchError((err) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text('An error occurred while logging out!'), content: Text('An error occurred while logging out!'),
)); ));
@ -191,13 +193,12 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
loadedCurrentUser = await UserAPIService(client).getCurrentUser(); loadedCurrentUser = await UserAPIService(client).getCurrentUser();
// load new token from server to avoid expiration // load new token from server to avoid expiration
String? newToken = await newUserService?.getToken(); String? newToken = await newUserService?.getToken();
if(newToken != null) { if (newToken != null) {
_storage.write(key: currentUser, value: newToken); _storage.write(key: currentUser, value: newToken);
client.configure(token: newToken); client.configure(token: newToken);
} }
} on ApiException catch (e) { } on ApiException catch (e) {
dev.log("Error code: " + e.errorCode.toString(),level: 1000); dev.log("Error code: " + e.errorCode.toString(), level: 1000);
if (e.errorCode ~/ 100 == 4) { if (e.errorCode ~/ 100 == 4) {
client.authenticated = false; client.authenticated = false;
if (e.errorCode == 401) { if (e.errorCode == 401) {
@ -227,7 +228,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
if (_loading) { if (_loading) {
return new Center(child: new CircularProgressIndicator()); return new Center(child: new CircularProgressIndicator());
} }
if(client.authenticated) { if (client.authenticated) {
notifications.scheduleDueNotifications(taskService); notifications.scheduleDueNotifications(taskService);
} }
return new VikunjaGlobalInherited( return new VikunjaGlobalInherited(

View File

@ -35,7 +35,8 @@ class IgnoreCertHttpOverrides extends HttpOverrides {
@pragma('vm:entry-point') @pragma('vm:entry-point')
void callbackDispatcher() { void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async { Workmanager().executeTask((task, inputData) async {
print("Native called background task: $task"); //simpleTask will be emitted here. print(
"Native called background task: $task"); //simpleTask will be emitted here.
if (task == "update-tasks" && inputData != null) { if (task == "update-tasks" && inputData != null) {
Client client = Client(null, Client client = Client(null,
token: inputData["client_token"], token: inputData["client_token"],
@ -56,7 +57,7 @@ void callbackDispatcher() {
.scheduleDueNotifications(taskService) .scheduleDueNotifications(taskService)
.then((value) => Future.value(true)); .then((value) => Future.value(true));
}); });
} else if( task == "refresh-token") { } else if (task == "refresh-token") {
print("running refresh from workmanager"); print("running refresh from workmanager");
final FlutterSecureStorage _storage = new FlutterSecureStorage(); final FlutterSecureStorage _storage = new FlutterSecureStorage();
@ -66,25 +67,24 @@ void callbackDispatcher() {
} }
var token = await _storage.read(key: currentUser); var token = await _storage.read(key: currentUser);
var base = await _storage.read(key: '${currentUser}_base'); var base = await _storage.read(key: '${currentUser}_base');
if (token == null || base == null) { if (token == null || base == null) {
return Future.value(true); return Future.value(true);
} }
Client client = Client(null); Client client = Client(null);
client.configure(token: token, base: base, authenticated: true); client.configure(token: token, base: base, authenticated: true);
// load new token from server to avoid expiration // load new token from server to avoid expiration
String? newToken = await UserAPIService(client).getToken(); String? newToken = await UserAPIService(client).getToken();
if(newToken != null) { if (newToken != null) {
_storage.write(key: currentUser, value: newToken); _storage.write(key: currentUser, value: newToken);
} }
return Future.value(true); return Future.value(true);
} else { } else {
return Future.value(true); return Future.value(true);
} }
}); });
} }
final globalSnackbarKey = GlobalKey<ScaffoldMessengerState>(); final globalSnackbarKey = GlobalKey<ScaffoldMessengerState>();
final globalNavigatorKey = GlobalKey<NavigatorState>(); final globalNavigatorKey = GlobalKey<NavigatorState>();
@ -108,60 +108,62 @@ void main() async {
key: UniqueKey(), key: UniqueKey(),
))); )));
} }
final ValueNotifier<bool> updateTheme = ValueNotifier(false); final ValueNotifier<bool> updateTheme = ValueNotifier(false);
class VikunjaApp extends StatelessWidget { class VikunjaApp extends StatelessWidget {
final Widget home; final Widget home;
final GlobalKey<NavigatorState>? navkey; final GlobalKey<NavigatorState>? navkey;
const VikunjaApp({Key? key, required this.home, this.navkey}) : super(key: key); const VikunjaApp({Key? key, required this.home, this.navkey})
: super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SettingsManager manager = SettingsManager(new FlutterSecureStorage()); SettingsManager manager = SettingsManager(new FlutterSecureStorage());
return new ValueListenableBuilder(
return new ValueListenableBuilder(valueListenable: updateTheme, builder: (_,mode,__) { valueListenable: updateTheme,
updateTheme.value = false; builder: (_, mode, __) {
FlutterThemeMode themeMode = FlutterThemeMode.system; updateTheme.value = false;
Future<ThemeData> theme = manager.getThemeMode().then((value) { FlutterThemeMode themeMode = FlutterThemeMode.system;
themeMode = value; Future<ThemeData> theme = manager.getThemeMode().then((value) {
switch(value) { themeMode = value;
case FlutterThemeMode.dark: switch (value) {
return buildVikunjaDarkTheme(); case FlutterThemeMode.dark:
case FlutterThemeMode.materialYouLight: return buildVikunjaDarkTheme();
return buildVikunjaMaterialLightTheme(); case FlutterThemeMode.materialYouLight:
case FlutterThemeMode.materialYouDark: return buildVikunjaMaterialLightTheme();
return buildVikunjaMaterialDarkTheme(); case FlutterThemeMode.materialYouDark:
default: return buildVikunjaMaterialDarkTheme();
return buildVikunjaTheme(); default:
} return buildVikunjaTheme();
}
}); });
return FutureBuilder<ThemeData>( return FutureBuilder<ThemeData>(
future: theme, future: theme,
builder: (BuildContext context, AsyncSnapshot<ThemeData> data) { builder: (BuildContext context, AsyncSnapshot<ThemeData> data) {
if(data.hasData) { if (data.hasData) {
return new DynamicColorBuilder(builder: (lightTheme, darkTheme) return new DynamicColorBuilder(
{ builder: (lightTheme, darkTheme) {
ThemeData? themeData = data.data; ThemeData? themeData = data.data;
if(themeMode == FlutterThemeMode.materialYouLight) if (themeMode == FlutterThemeMode.materialYouLight)
themeData = themeData?.copyWith(colorScheme: lightTheme); themeData = themeData?.copyWith(colorScheme: lightTheme);
else if(themeMode == FlutterThemeMode.materialYouDark) else if (themeMode == FlutterThemeMode.materialYouDark)
themeData = themeData?.copyWith(colorScheme: darkTheme); themeData = themeData?.copyWith(colorScheme: darkTheme);
return MaterialApp( return MaterialApp(
title: 'Vikunja', title: 'Vikunja',
theme: themeData, theme: themeData,
scaffoldMessengerKey: globalSnackbarKey, scaffoldMessengerKey: globalSnackbarKey,
navigatorKey: navkey, navigatorKey: navkey,
// <= this // <= this
home: this.home, home: this.home,
); );
}); });
} else { } else {
return Center(child: CircularProgressIndicator()); return Center(child: CircularProgressIndicator());
} }
});}); });
});
} }
} }

View File

@ -5,7 +5,8 @@ import 'dart:math';
import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/timezone.dart' as tz; import 'package:timezone/timezone.dart' as tz;
import 'package:flutter_local_notifications/flutter_local_notifications.dart'as notifs; import 'package:flutter_local_notifications/flutter_local_notifications.dart'
as notifs;
import 'package:rxdart/subjects.dart' as rxSub; import 'package:rxdart/subjects.dart' as rxSub;
import 'package:vikunja_app/service/services.dart'; import 'package:vikunja_app/service/services.dart';
@ -19,57 +20,52 @@ class NotificationClass {
late String currentTimeZone; late String currentTimeZone;
notifs.NotificationAppLaunchDetails? notifLaunch; notifs.NotificationAppLaunchDetails? notifLaunch;
notifs.FlutterLocalNotificationsPlugin get notificationsPlugin => new notifs.FlutterLocalNotificationsPlugin(); notifs.FlutterLocalNotificationsPlugin get notificationsPlugin =>
new notifs.FlutterLocalNotificationsPlugin();
var androidSpecificsDueDate = notifs.AndroidNotificationDetails( var androidSpecificsDueDate = notifs.AndroidNotificationDetails(
"Vikunja1", "Vikunja1", "Due Date Notifications",
"Due Date Notifications",
channelDescription: "description", channelDescription: "description",
icon: 'vikunja_notification_logo', icon: 'vikunja_notification_logo',
importance: notifs.Importance.high importance: notifs.Importance.high);
);
var androidSpecificsReminders = notifs.AndroidNotificationDetails( var androidSpecificsReminders = notifs.AndroidNotificationDetails(
"Vikunja2", "Vikunja2", "Reminder Notifications",
"Reminder Notifications",
channelDescription: "description", channelDescription: "description",
icon: 'vikunja_notification_logo', icon: 'vikunja_notification_logo',
importance: notifs.Importance.high importance: notifs.Importance.high);
);
late notifs.IOSNotificationDetails iOSSpecifics; late notifs.IOSNotificationDetails iOSSpecifics;
late notifs.NotificationDetails platformChannelSpecificsDueDate; late notifs.NotificationDetails platformChannelSpecificsDueDate;
late notifs.NotificationDetails platformChannelSpecificsReminders; late notifs.NotificationDetails platformChannelSpecificsReminders;
NotificationClass({this.id, this.body, this.payload, this.title}); NotificationClass({this.id, this.body, this.payload, this.title});
final rxSub.BehaviorSubject< final rxSub.BehaviorSubject<NotificationClass>
NotificationClass> didReceiveLocalNotificationSubject = didReceiveLocalNotificationSubject =
rxSub.BehaviorSubject<NotificationClass>(); rxSub.BehaviorSubject<NotificationClass>();
final rxSub.BehaviorSubject<String> selectNotificationSubject = final rxSub.BehaviorSubject<String> selectNotificationSubject =
rxSub.BehaviorSubject<String>(); rxSub.BehaviorSubject<String>();
Future<void> _initNotifications() async { Future<void> _initNotifications() async {
var initializationSettingsAndroid = var initializationSettingsAndroid =
notifs.AndroidInitializationSettings('vikunja_logo'); notifs.AndroidInitializationSettings('vikunja_logo');
var initializationSettingsIOS = notifs.IOSInitializationSettings( var initializationSettingsIOS = notifs.IOSInitializationSettings(
requestAlertPermission: false, requestAlertPermission: false,
requestBadgePermission: false, requestBadgePermission: false,
requestSoundPermission: false, requestSoundPermission: false,
onDidReceiveLocalNotification: onDidReceiveLocalNotification:
(int? id, String? title, String? body, String? payload) async { (int? id, String? title, String? body, String? payload) async {
didReceiveLocalNotificationSubject didReceiveLocalNotificationSubject.add(NotificationClass(
.add(NotificationClass(
id: id, title: title, body: body, payload: payload)); id: id, title: title, body: body, payload: payload));
}); });
var initializationSettings = notifs.InitializationSettings( var initializationSettings = notifs.InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS); android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await notificationsPlugin.initialize(initializationSettings, await notificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String? payload) async { onSelectNotification: (String? payload) async {
if (payload != null) { if (payload != null) {
print('notification payload: ' + payload); print('notification payload: ' + payload);
selectNotificationSubject.add(payload); selectNotificationSubject.add(payload);
} }
}); });
print("Notifications initialised successfully"); print("Notifications initialised successfully");
} }
@ -86,20 +82,23 @@ class NotificationClass {
return Future.value(); return Future.value();
} }
Future<void> scheduleNotification(String title, String description, Future<void> scheduleNotification(
String title,
String description,
notifs.FlutterLocalNotificationsPlugin notifsPlugin, notifs.FlutterLocalNotificationsPlugin notifsPlugin,
DateTime scheduledTime, String currentTimeZone, DateTime scheduledTime,
notifs.NotificationDetails platformChannelSpecifics, {int? id}) async { String currentTimeZone,
if (id == null) notifs.NotificationDetails platformChannelSpecifics,
id = Random().nextInt(1000000); {int? id}) async {
if (id == null) id = Random().nextInt(1000000);
// TODO: move to setup // TODO: move to setup
tz.TZDateTime time = tz.TZDateTime.from( tz.TZDateTime time =
scheduledTime, tz.getLocation(currentTimeZone)); tz.TZDateTime.from(scheduledTime, tz.getLocation(currentTimeZone));
if (time.difference(tz.TZDateTime.now(tz.getLocation(currentTimeZone))) < if (time.difference(tz.TZDateTime.now(tz.getLocation(currentTimeZone))) <
Duration.zero) Duration.zero) return;
return; await notifsPlugin.zonedSchedule(
await notifsPlugin.zonedSchedule(id, title, description, id, title, description, time, platformChannelSpecifics,
time, platformChannelSpecifics, androidAllowWhileIdle: true, androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation: notifs uiLocalNotificationDateInterpretation: notifs
.UILocalNotificationDateInterpretation .UILocalNotificationDateInterpretation
.wallClockTime); // This literally schedules the notification .wallClockTime); // This literally schedules the notification
@ -110,30 +109,27 @@ class NotificationClass {
"This is a test notification", platformChannelSpecificsReminders); "This is a test notification", platformChannelSpecificsReminders);
} }
void requestIOSPermissions() { void requestIOSPermissions() {
notificationsPlugin.resolvePlatformSpecificImplementation< notificationsPlugin
notifs.IOSFlutterLocalNotificationsPlugin>() .resolvePlatformSpecificImplementation<
notifs.IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions( ?.requestPermissions(
alert: true, alert: true,
badge: true, badge: true,
sound: true, sound: true,
); );
} }
Future<void> scheduleDueNotifications(TaskService taskService, Future<void> scheduleDueNotifications(TaskService taskService,
{List<Task>? tasks}) async { {List<Task>? tasks}) async {
if (tasks == null) if (tasks == null) tasks = await taskService.getAll();
tasks = await taskService.getAll();
if (tasks == null) { if (tasks == null) {
print("did not receive tasks on notification update"); print("did not receive tasks on notification update");
return; return;
} }
await notificationsPlugin.cancelAll(); await notificationsPlugin.cancelAll();
for (final task in tasks) { for (final task in tasks) {
if(task.done) if (task.done) continue;
continue;
for (final reminder in task.reminderDates) { for (final reminder in task.reminderDates) {
scheduleNotification( scheduleNotification(
"Reminder", "Reminder",
@ -159,5 +155,4 @@ class NotificationClass {
} }
print("notifications scheduled successfully"); print("notifications scheduled successfully");
} }
}
}

View File

@ -47,15 +47,15 @@ class Bucket {
.toList(); .toList();
toJSON() => { toJSON() => {
'id': id, 'id': id,
'list_id': projectId, 'list_id': projectId,
'title': title, 'title': title,
'position': position, 'position': position,
'limit': limit, 'limit': limit,
'is_done_bucket': isDoneBucket, 'is_done_bucket': isDoneBucket,
'created': created.toUtc().toIso8601String(), 'created': created.toUtc().toIso8601String(),
'updated': updated.toUtc().toIso8601String(), 'updated': updated.toUtc().toIso8601String(),
'created_by': createdBy.toJSON(), 'created_by': createdBy.toJSON(),
'tasks': tasks.map((task) => task.toJSON()).toList(), 'tasks': tasks.map((task) => task.toJSON()).toList(),
}; };
} }

View File

@ -10,7 +10,9 @@ class Label {
final User createdBy; final User createdBy;
final Color? color; final Color? color;
late final Color textColor = color != null && color!.computeLuminance() <= 0.5 ? vLabelLight : vLabelDark; late final Color textColor = color != null && color!.computeLuminance() <= 0.5
? vLabelLight
: vLabelDark;
Label({ Label({
this.id = 0, this.id = 0,

View File

@ -10,7 +10,8 @@ class LabelTask {
LabelTask({required this.label, required this.task}); LabelTask({required this.label, required this.task});
LabelTask.fromJson(Map<String, dynamic> json, User createdBy) LabelTask.fromJson(Map<String, dynamic> json, User createdBy)
: label = new Label(id: json['label_id'], title: '', createdBy: createdBy), : label =
new Label(id: json['label_id'], title: '', createdBy: createdBy),
task = null; task = null;
toJSON() => { toJSON() => {

View File

@ -33,9 +33,11 @@ class TaskList {
created = DateTime.parse(json['created']), created = DateTime.parse(json['created']),
isFavorite = json['is_favorite'], isFavorite = json['is_favorite'],
namespaceId = json['namespace_id'], namespaceId = json['namespace_id'],
tasks = json['tasks'] == null ? [] : (json['tasks'] as List<dynamic>) tasks = json['tasks'] == null
.map((taskJson) => Task.fromJson(taskJson)) ? []
.toList(); : (json['tasks'] as List<dynamic>)
.map((taskJson) => Task.fromJson(taskJson))
.toList();
toJSON() { toJSON() {
return { return {

View File

@ -17,20 +17,19 @@ class Project {
Iterable<Project>? subprojects; Iterable<Project>? subprojects;
Project( Project(
{ {this.id = 0,
this.id = 0, this.owner,
this.owner, this.parentProjectId = 0,
this.parentProjectId = 0, this.description = '',
this.description = '', this.position = 0,
this.position = 0, this.doneBucketId,
this.doneBucketId, this.color,
this.color, this.isArchived = false,
this.isArchived = false, this.isFavourite = false,
this.isFavourite = false, required this.title,
required this.title,
created, created,
updated}) : updated})
this.created = created ?? DateTime.now(), : this.created = created ?? DateTime.now(),
this.updated = updated ?? DateTime.now(); this.updated = updated ?? DateTime.now();
Project.fromJson(Map<String, dynamic> json) Project.fromJson(Map<String, dynamic> json)
@ -50,19 +49,20 @@ class Project {
owner = json['owner'] != null ? User.fromJson(json['owner']) : null; owner = json['owner'] != null ? User.fromJson(json['owner']) : null;
Map<String, dynamic> toJSON() => { Map<String, dynamic> toJSON() => {
'id': id, 'id': id,
'created': created.toUtc().toIso8601String(), 'created': created.toUtc().toIso8601String(),
'updated': updated.toUtc().toIso8601String(), 'updated': updated.toUtc().toIso8601String(),
'title': title, 'title': title,
'owner': owner?.toJSON(), 'owner': owner?.toJSON(),
'description': description, 'description': description,
'parent_project_id': parentProjectId, 'parent_project_id': parentProjectId,
'hex_color': color?.value.toRadixString(16).padLeft(8, '0').substring(2), 'hex_color':
'is_archived': isArchived, color?.value.toRadixString(16).padLeft(8, '0').substring(2),
'is_favourite': isFavourite, 'is_archived': isArchived,
'done_bucket_id': doneBucketId, 'is_favourite': isFavourite,
'position': position 'done_bucket_id': doneBucketId,
}; 'position': position
};
Project copyWith({ Project copyWith({
int? id, int? id,
@ -77,21 +77,20 @@ class Project {
bool? isFavourite, bool? isFavourite,
int? doneBucketId, int? doneBucketId,
double? position, double? position,
}) { }) {
return Project( return Project(
id: id ?? this.id, id: id ?? this.id,
created: created ?? this.created, created: created ?? this.created,
updated: updated ?? this.updated, updated: updated ?? this.updated,
title: title ?? this.title, title: title ?? this.title,
owner: owner ?? this.owner, owner: owner ?? this.owner,
description: description ?? this.description, description: description ?? this.description,
parentProjectId: parentProjectId ?? this.parentProjectId, parentProjectId: parentProjectId ?? this.parentProjectId,
doneBucketId: doneBucketId ?? this.doneBucketId, doneBucketId: doneBucketId ?? this.doneBucketId,
color: color ?? this.color, color: color ?? this.color,
isArchived: isArchived ?? this.isArchived, isArchived: isArchived ?? this.isArchived,
isFavourite: isFavourite ?? this.isFavourite, isFavourite: isFavourite ?? this.isFavourite,
position: position ?? this.position, position: position ?? this.position,
); );
} }
} }

View File

@ -13,8 +13,7 @@ class Server {
String? version; String? version;
Server.fromJson(Map<String, dynamic> json) Server.fromJson(Map<String, dynamic> json)
: : caldavEnabled = json['caldav_enabled'],
caldavEnabled = json['caldav_enabled'],
emailRemindersEnabled = json['email_reminders_enabled'], emailRemindersEnabled = json['email_reminders_enabled'],
frontendUrl = json['frontend_url'], frontendUrl = json['frontend_url'],
linkSharingEnabled = json['link_sharing_enabled'], linkSharingEnabled = json['link_sharing_enabled'],
@ -26,4 +25,4 @@ class Server {
totpEnabled = json['totp_enabled'], totpEnabled = json['totp_enabled'],
userDeletion = json['user_deletion'], userDeletion = json['user_deletion'],
version = json['version']; version = json['version'];
} }

View File

@ -67,6 +67,7 @@ class Task {
} }
return Colors.white; return Colors.white;
} }
bool get hasDueDate => dueDate?.year != 1; bool get hasDueDate => dueDate?.year != 1;
Task.fromJson(Map<String, dynamic> json) Task.fromJson(Map<String, dynamic> json)
@ -131,7 +132,8 @@ class Task {
'end_date': endDate?.toUtc().toIso8601String(), 'end_date': endDate?.toUtc().toIso8601String(),
'priority': priority, 'priority': priority,
'repeat_after': repeatAfter?.inSeconds, 'repeat_after': repeatAfter?.inSeconds,
'hex_color': color?.value.toRadixString(16).padLeft(8, '0').substring(2), 'hex_color':
color?.value.toRadixString(16).padLeft(8, '0').substring(2),
'kanban_position': kanbanPosition, 'kanban_position': kanbanPosition,
'percent_done': percent_done, 'percent_done': percent_done,
'project_id': projectId, 'project_id': projectId,

View File

@ -25,15 +25,14 @@ class TaskAttachmentFile {
size = json['size']; size = json['size'];
toJSON() => { toJSON() => {
'id': id, 'id': id,
'created': created.toUtc().toIso8601String(), 'created': created.toUtc().toIso8601String(),
'mime': mime, 'mime': mime,
'name': name, 'name': name,
'size': size, 'size': size,
}; };
} }
@JsonSerializable() @JsonSerializable()
class TaskAttachment { class TaskAttachment {
final int id, taskId; final int id, taskId;
@ -58,10 +57,10 @@ class TaskAttachment {
createdBy = User.fromJson(json['created_by']); createdBy = User.fromJson(json['created_by']);
toJSON() => { toJSON() => {
'id': id, 'id': id,
'task_id': taskId, 'task_id': taskId,
'created': created.toUtc().toIso8601String(), 'created': created.toUtc().toIso8601String(),
'created_by': createdBy.toJSON(), 'created_by': createdBy.toJSON(),
'file': file.toJSON(), 'file': file.toJSON(),
}; };
} }

View File

@ -3,7 +3,9 @@ import 'package:vikunja_app/global.dart';
class UserSettings { class UserSettings {
final int default_project_id; final int default_project_id;
final bool discoverable_by_email, discoverable_by_name, email_reminders_enabled; final bool discoverable_by_email,
discoverable_by_name,
email_reminders_enabled;
final Map<String, dynamic>? frontend_settings; final Map<String, dynamic>? frontend_settings;
final String language; final String language;
final String name; final String name;
@ -34,24 +36,25 @@ class UserSettings {
frontend_settings = json['frontend_settings'], frontend_settings = json['frontend_settings'],
language = json['language'], language = json['language'],
name = json['name'], name = json['name'],
overdue_tasks_reminders_enabled = json['overdue_tasks_reminders_enabled'], overdue_tasks_reminders_enabled =
json['overdue_tasks_reminders_enabled'],
overdue_tasks_reminders_time = json['overdue_tasks_reminders_time'], overdue_tasks_reminders_time = json['overdue_tasks_reminders_time'],
timezone = json['timezone'], timezone = json['timezone'],
week_start = json['week_start']; week_start = json['week_start'];
toJson() => { toJson() => {
'default_project_id': default_project_id, 'default_project_id': default_project_id,
'discoverable_by_email': discoverable_by_email, 'discoverable_by_email': discoverable_by_email,
'discoverable_by_name': discoverable_by_name, 'discoverable_by_name': discoverable_by_name,
'email_reminders_enabled': email_reminders_enabled, 'email_reminders_enabled': email_reminders_enabled,
'frontend_settings': frontend_settings, 'frontend_settings': frontend_settings,
'language': language, 'language': language,
'name': name, 'name': name,
'overdue_tasks_reminders_enabled': overdue_tasks_reminders_enabled, 'overdue_tasks_reminders_enabled': overdue_tasks_reminders_enabled,
'overdue_tasks_reminders_time': overdue_tasks_reminders_time, 'overdue_tasks_reminders_time': overdue_tasks_reminders_time,
'timezone': timezone, 'timezone': timezone,
'week_start': week_start, 'week_start': week_start,
}; };
UserSettings copyWith({ UserSettings copyWith({
int? default_project_id, int? default_project_id,
@ -68,14 +71,18 @@ class UserSettings {
}) { }) {
return UserSettings( return UserSettings(
default_project_id: default_project_id ?? this.default_project_id, default_project_id: default_project_id ?? this.default_project_id,
discoverable_by_email: discoverable_by_email ?? this.discoverable_by_email, discoverable_by_email:
discoverable_by_email ?? this.discoverable_by_email,
discoverable_by_name: discoverable_by_name ?? this.discoverable_by_name, discoverable_by_name: discoverable_by_name ?? this.discoverable_by_name,
email_reminders_enabled: email_reminders_enabled ?? this.email_reminders_enabled, email_reminders_enabled:
email_reminders_enabled ?? this.email_reminders_enabled,
frontend_settings: frontend_settings ?? this.frontend_settings, frontend_settings: frontend_settings ?? this.frontend_settings,
language: language ?? this.language, language: language ?? this.language,
name: name ?? this.name, name: name ?? this.name,
overdue_tasks_reminders_enabled: overdue_tasks_reminders_enabled ?? this.overdue_tasks_reminders_enabled, overdue_tasks_reminders_enabled: overdue_tasks_reminders_enabled ??
overdue_tasks_reminders_time: overdue_tasks_reminders_time ?? this.overdue_tasks_reminders_time, this.overdue_tasks_reminders_enabled,
overdue_tasks_reminders_time:
overdue_tasks_reminders_time ?? this.overdue_tasks_reminders_time,
timezone: timezone ?? this.timezone, timezone: timezone ?? this.timezone,
week_start: week_start ?? this.week_start, week_start: week_start ?? this.week_start,
); );
@ -104,9 +111,10 @@ class User {
username = json['username'], username = json['username'],
created = DateTime.parse(json['created']), created = DateTime.parse(json['created']),
updated = DateTime.parse(json['updated']) { updated = DateTime.parse(json['updated']) {
if(json.containsKey('settings')){ if (json.containsKey('settings')) {
this.settings = UserSettings.fromJson(json['settings']); this.settings = UserSettings.fromJson(json['settings']);
}; }
;
} }
toJSON() => { toJSON() => {

View File

@ -28,7 +28,6 @@ class HomePageState extends State<HomePage> {
int _selectedDrawerIndex = 0, _previousDrawerIndex = 0; int _selectedDrawerIndex = 0, _previousDrawerIndex = 0;
Widget? drawerItem; Widget? drawerItem;
List<Widget> widgets = [ List<Widget> widgets = [
ChangeNotifierProvider<ProjectProvider>( ChangeNotifierProvider<ProjectProvider>(
create: (_) => new ProjectProvider(), create: (_) => new ProjectProvider(),

View File

@ -37,11 +37,11 @@ class LandingPageState extends State<LandingPage>
static const platform = const MethodChannel('vikunja'); static const platform = const MethodChannel('vikunja');
Future<void> _updateDefaultList() async { Future<void> _updateDefaultList() async {
return VikunjaGlobal.of(context).newUserService?.getCurrentUser().then(
return VikunjaGlobal.of(context).newUserService?.getCurrentUser().then((value) => (value) => setState(() {
setState(() { defaultList = value?.settings?.default_project_id;
defaultList = value?.settings?.default_project_id; }),
} ),); );
} }
@override @override
@ -55,7 +55,10 @@ class LandingPageState extends State<LandingPage>
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
} }
VikunjaGlobal.of(context).settingsManager.getLandingPageOnlyDueDateTasks().then((value) => onlyDueDate = value); VikunjaGlobal.of(context)
.settingsManager
.getLandingPageOnlyDueDateTasks()
.then((value) => onlyDueDate = value);
})); }));
super.initState(); super.initState();
} }
@ -133,26 +136,29 @@ class LandingPageState extends State<LandingPage>
PopupMenuButton(itemBuilder: (BuildContext context) { PopupMenuButton(itemBuilder: (BuildContext context) {
return [ return [
PopupMenuItem( PopupMenuItem(
child: child: InkWell(
InkWell( onTap: () {
onTap: () { Navigator.pop(context);
Navigator.pop(context); bool newval = !onlyDueDate;
bool newval = !onlyDueDate; VikunjaGlobal.of(context)
VikunjaGlobal.of(context).settingsManager.setLandingPageOnlyDueDateTasks(newval).then((value) { .settingsManager
setState(() { .setLandingPageOnlyDueDateTasks(newval)
onlyDueDate = newval; .then((value) {
_loadList(context); setState(() {
}); onlyDueDate = newval;
}); _loadList(context);
}, });
child: });
Row(mainAxisAlignment: MainAxisAlignment.end, children: [ },
Text("Only show tasks with due date"), child: Row(
Checkbox( mainAxisAlignment: MainAxisAlignment.end,
value: onlyDueDate, children: [
onChanged: (bool? value) { }, Text("Only show tasks with due date"),
) Checkbox(
]))) value: onlyDueDate,
onChanged: (bool? value) {},
)
])))
]; ];
}), }),
], ],
@ -226,41 +232,47 @@ class LandingPageState extends State<LandingPage>
.settingsManager .settingsManager
.getLandingPageOnlyDueDateTasks() .getLandingPageOnlyDueDateTasks()
.then((showOnlyDueDateTasks) { .then((showOnlyDueDateTasks) {
VikunjaGlobalState global = VikunjaGlobal.of(context);
Map<String, dynamic>? frontend_settings =
global.currentUser?.settings?.frontend_settings;
int? filterId = 0;
if (frontend_settings != null) {
if (frontend_settings["filter_id_used_on_overview"] != null)
filterId = frontend_settings["filter_id_used_on_overview"];
}
if (filterId != null && filterId != 0) {
return global.taskService.getAllByProject(filterId, {
"sort_by": ["due_date", "id"],
"order_by": ["asc", "desc"],
}).then<Future<void>?>((response) =>
_handleTaskList(response?.body, showOnlyDueDateTasks));
;
}
VikunjaGlobalState global = VikunjaGlobal.of(context); return global.taskService
Map<String, dynamic>? frontend_settings = global.currentUser?.settings?.frontend_settings; .getByOptions(TaskServiceOptions(newOptions: [
int? filterId = 0; TaskServiceOption<TaskServiceOptionSortBy>(
if(frontend_settings != null) { "sort_by", ["due_date", "id"]),
if(frontend_settings["filter_id_used_on_overview"] != null) TaskServiceOption<TaskServiceOptionSortBy>(
filterId = frontend_settings["filter_id_used_on_overview"]; "order_by", ["asc", "desc"]),
} TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", "done"),
if(filterId != null && filterId != 0) { TaskServiceOption<TaskServiceOptionFilterValue>(
return global.taskService.getAllByProject(filterId, { "filter_value", "false"),
"sort_by": ["due_date", "id"], TaskServiceOption<TaskServiceOptionFilterComparator>(
"order_by": ["asc", "desc"], "filter_comparator", "equals"),
}).then<Future<void>?>((response) => _handleTaskList(response?.body, showOnlyDueDateTasks));; TaskServiceOption<TaskServiceOptionFilterConcat>(
} "filter_concat", "and"),
], clearOther: true))
return global.taskService .then<Future<void>?>(
.getByOptions(TaskServiceOptions( (taskList) => _handleTaskList(taskList, showOnlyDueDateTasks));
newOptions: [ }); //.onError((error, stackTrace) {print("error");});
TaskServiceOption<TaskServiceOptionSortBy>("sort_by", ["due_date", "id"]),
TaskServiceOption<TaskServiceOptionSortBy>("order_by", ["asc", "desc"]),
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", "done"),
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value", "false"),
TaskServiceOption<TaskServiceOptionFilterComparator>("filter_comparator", "equals"),
TaskServiceOption<TaskServiceOptionFilterConcat>("filter_concat", "and"),
],
clearOther: true
))
.then<Future<void>?>((taskList) => _handleTaskList(taskList, showOnlyDueDateTasks));
});//.onError((error, stackTrace) {print("error");});
} }
Future<void> _handleTaskList(List<Task>? taskList, bool showOnlyDueDateTasks) { Future<void> _handleTaskList(
if(showOnlyDueDateTasks) List<Task>? taskList, bool showOnlyDueDateTasks) {
taskList?.removeWhere((element) => element.dueDate == null || element.dueDate!.year == 0001); if (showOnlyDueDateTasks)
taskList?.removeWhere((element) =>
element.dueDate == null || element.dueDate!.year == 0001);
if (taskList != null && taskList.isEmpty) { if (taskList != null && taskList.isEmpty) {
setState(() { setState(() {

View File

@ -66,7 +66,7 @@ class _ListPageState extends State<ListPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
taskState = Provider.of<ListProvider>(context); taskState = Provider.of<ListProvider>(context);
//_kanban = KanbanClass( //_kanban = KanbanClass(
// context, nullSetState, _onViewTapped, _addItemDialog, _list); // context, nullSetState, _onViewTapped, _addItemDialog, _list);
Widget body; Widget body;
@ -127,10 +127,8 @@ class _ListPageState extends State<ListPage> {
]); ]);
break; break;
case PageStatus.empty: case PageStatus.empty:
body = new Stack(children: [ body = new Stack(
ListView(), children: [ListView(), Center(child: Text("This view is empty"))]);
Center(child: Text("This view is empty"))
]);
break; break;
} }
@ -242,8 +240,8 @@ class _ListPageState extends State<ListPage> {
TaskTile _buildLoadingTile(Task task) { TaskTile _buildLoadingTile(Task task) {
return TaskTile( return TaskTile(
task: task, task: task,
loading: true, onEdit: () {}, loading: true,
onEdit: () {},
); );
} }
@ -256,13 +254,11 @@ class _ListPageState extends State<ListPage> {
_loadTasksForPage(1); _loadTasksForPage(1);
break; break;
case 1: case 1:
await _kanban await _kanban.loadBucketsForPage(1);
.loadBucketsForPage(1);
// load all buckets to get length for RecordableListView // load all buckets to get length for RecordableListView
while (_currentPage < taskState.maxPages) { while (_currentPage < taskState.maxPages) {
_currentPage++; _currentPage++;
await _kanban await _kanban.loadBucketsForPage(_currentPage);
.loadBucketsForPage(_currentPage);
} }
break; break;
default: default:
@ -272,12 +268,11 @@ class _ListPageState extends State<ListPage> {
} }
Future<void> _loadTasksForPage(int page) { Future<void> _loadTasksForPage(int page) {
return Provider.of<ListProvider>(context, listen: false) return Provider.of<ListProvider>(context, listen: false).loadTasks(
.loadTasks( context: context,
context: context, listId: _list.id,
listId: _list.id, page: page,
page: page, displayDoneTasks: displayDoneTasks);
displayDoneTasks: displayDoneTasks);
} }
Future<void> _addItemDialog(BuildContext context, [Bucket? bucket]) { Future<void> _addItemDialog(BuildContext context, [Bucket? bucket]) {

View File

@ -24,16 +24,18 @@ class _ListEditPageState extends State<ListEditPage> {
late int listId; late int listId;
@override @override
void initState(){ void initState() {
listId = widget.list.id; listId = widget.list.id;
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext ctx) { Widget build(BuildContext ctx) {
if(displayDoneTasks == null) if (displayDoneTasks == null)
VikunjaGlobal.of(context).listService.getDisplayDoneTasks(listId).then( VikunjaGlobal.of(context)
(value) => setState(() => displayDoneTasks = value == "1")); .listService
.getDisplayDoneTasks(listId)
.then((value) => setState(() => displayDoneTasks = value == "1"));
else else
log("Display done tasks: " + displayDoneTasks.toString()); log("Display done tasks: " + displayDoneTasks.toString());
return Scaffold( return Scaffold(
@ -45,7 +47,7 @@ class _ListEditPageState extends State<ListEditPage> {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: ListView( child: ListView(
//reverse: true, //reverse: true,
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: <Widget>[ children: <Widget>[
Padding( Padding(
@ -73,10 +75,10 @@ class _ListEditPageState extends State<ListEditPage> {
maxLines: null, maxLines: null,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
initialValue: widget.list.description, initialValue: widget.list.description,
onSaved: (description) => _description = description ?? '', onSaved: (description) =>
_description = description ?? '',
validator: (description) { validator: (description) {
if(description == null) if (description == null) return null;
return null;
if (description.length > 1000) { if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.'; return 'The description can have a maximum of 1000 characters.';
} }
@ -95,7 +97,9 @@ class _ListEditPageState extends State<ListEditPage> {
title: Text("Show done tasks"), title: Text("Show done tasks"),
onChanged: (value) { onChanged: (value) {
value ??= false; value ??= false;
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value ? "1" : "0"); VikunjaGlobal.of(context)
.listService
.setDisplayDoneTasks(listId, value ? "1" : "0");
setState(() => displayDoneTasks = value); setState(() => displayDoneTasks = value);
}, },
), ),
@ -137,8 +141,7 @@ class _ListEditPageState extends State<ListEditPage> {
},) },)
], ],
)*/ )*/
] ]),
),
), ),
), ),
), ),

View File

@ -48,8 +48,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
@override @override
void initState() { void initState() {
_repeatAfter = widget.task.repeatAfter; _repeatAfter = widget.task.repeatAfter;
if(_repeatAfterType == null) if (_repeatAfterType == null)
_repeatAfterType = getRepeatAfterTypeFromDuration(_repeatAfter); _repeatAfterType = getRepeatAfterTypeFromDuration(_repeatAfter);
_reminderDates = widget.task.reminderDates; _reminderDates = widget.task.reminderDates;
for (var i = 0; i < _reminderDates.length; i++) { for (var i = 0; i < _reminderDates.length; i++) {
@ -86,25 +86,30 @@ class _TaskEditPageState extends State<TaskEditPage> {
actions: [ actions: [
IconButton( IconButton(
icon: Icon(Icons.delete), icon: Icon(Icons.delete),
onPressed: () {showDialog(context: context, builder: (BuildContext context) { onPressed: () {
return AlertDialog( showDialog(
title: Text('Delete Task'), context: context,
content: Text('Are you sure you want to delete this task?'), builder: (BuildContext context) {
actions: [ return AlertDialog(
TextButton( title: Text('Delete Task'),
child: Text('Cancel'), content: Text(
onPressed: () => Navigator.of(context).pop(), 'Are you sure you want to delete this task?'),
), actions: [
TextButton( TextButton(
child: Text('Delete'), child: Text('Cancel'),
onPressed: () { onPressed: () => Navigator.of(context).pop(),
_delete(widget.task.id); ),
Navigator.of(context).pop(); TextButton(
}, child: Text('Delete'),
), onPressed: () {
], _delete(widget.task.id);
); Navigator.of(context).pop();
});}, },
),
],
);
});
},
), ),
], ],
), ),
@ -114,7 +119,8 @@ class _TaskEditPageState extends State<TaskEditPage> {
key: _formKey, key: _formKey,
child: ListView( child: ListView(
key: _listKey, key: _listKey,
padding: EdgeInsets.fromLTRB(16, 16, 16, MediaQuery.of(context).size.height / 2), padding: EdgeInsets.fromLTRB(
16, 16, 16, MediaQuery.of(context).size.height / 2),
children: <Widget>[ children: <Widget>[
Padding( Padding(
padding: EdgeInsets.symmetric(vertical: 10.0), padding: EdgeInsets.symmetric(vertical: 10.0),
@ -182,8 +188,9 @@ class _TaskEditPageState extends State<TaskEditPage> {
flex: 2, flex: 2,
child: TextFormField( child: TextFormField(
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
initialValue: getRepeatAfterValueFromDuration( initialValue:
_repeatAfter)?.toString(), getRepeatAfterValueFromDuration(_repeatAfter)
?.toString(),
onSaved: (repeatAfter) => _repeatAfter = onSaved: (repeatAfter) => _repeatAfter =
getDurationFromType( getDurationFromType(
repeatAfter, _repeatAfterType), repeatAfter, _repeatAfterType),
@ -200,8 +207,7 @@ class _TaskEditPageState extends State<TaskEditPage> {
isExpanded: true, isExpanded: true,
isDense: true, isDense: true,
value: _repeatAfterType ?? value: _repeatAfterType ??
getRepeatAfterTypeFromDuration( getRepeatAfterTypeFromDuration(_repeatAfter),
_repeatAfter),
onChanged: (String? newValue) { onChanged: (String? newValue) {
setState(() { setState(() {
_repeatAfterType = newValue; _repeatAfterType = newValue;
@ -321,9 +327,11 @@ class _TaskEditPageState extends State<TaskEditPage> {
Padding( Padding(
padding: EdgeInsets.only( padding: EdgeInsets.only(
right: 15, right: 15,
left: 2.0 + (IconTheme.of(context).size??0))), left: 2.0 + (IconTheme.of(context).size ?? 0))),
Container( Container(
width: MediaQuery.of(context).size.width - 80 - ((IconTheme.of(context).size ?? 0) * 2), width: MediaQuery.of(context).size.width -
80 -
((IconTheme.of(context).size ?? 0) * 2),
child: TypeAheadField( child: TypeAheadField(
builder: (builder, controller, focusnode) { builder: (builder, controller, focusnode) {
return TextFormField( return TextFormField(
@ -427,18 +435,25 @@ class _TaskEditPageState extends State<TaskEditPage> {
trailing: IconButton( trailing: IconButton(
icon: Icon(Icons.download), icon: Icon(Icons.download),
onPressed: () async { onPressed: () async {
String url = VikunjaGlobal.of(context).client.base; String url =
url += '/tasks/${widget.task.id}/attachments/${widget.task.attachments[index].id}'; VikunjaGlobal.of(context).client.base;
url +=
'/tasks/${widget.task.id}/attachments/${widget.task.attachments[index].id}';
print(url); print(url);
final taskId = await FlutterDownloader.enqueue( final taskId = await FlutterDownloader.enqueue(
url: url, url: url,
fileName: widget.task.attachments[index].file.name, fileName:
headers: VikunjaGlobal.of(context).client.headers, // optional: header send with url (auth token etc) widget.task.attachments[index].file.name,
headers: VikunjaGlobal.of(context)
.client
.headers, // optional: header send with url (auth token etc)
savedDir: '/storage/emulated/0/Download/', savedDir: '/storage/emulated/0/Download/',
showNotification: true, // show download progress in status bar (for Android) showNotification:
openFileFromNotification: true, // click on notification to open downloaded file (for Android) true, // show download progress in status bar (for Android)
openFileFromNotification:
true, // click on notification to open downloaded file (for Android)
); );
if(taskId == null) return; if (taskId == null) return;
FlutterDownloader.open(taskId: taskId); FlutterDownloader.open(taskId: taskId);
}, },
), ),
@ -594,8 +609,6 @@ class _TaskEditPageState extends State<TaskEditPage> {
}); });
} }
_onColorEdit() { _onColorEdit() {
_pickerColor = _resetColor || (_color ?? widget.task.color) == null _pickerColor = _resetColor || (_color ?? widget.task.color) == null
? Colors.black ? Colors.black

View File

@ -84,10 +84,8 @@ class _NamespacePageState extends State<NamespacePage> {
); );
break; break;
case PageStatus.empty: case PageStatus.empty:
body = new Stack(children: [ body = new Stack(
ListView(), children: [ListView(), Center(child: Text("This view is empty"))]);
Center(child: Text("This view is empty"))
]);
break; break;
} }
return new Scaffold( return new Scaffold(
@ -112,7 +110,6 @@ class _NamespacePageState extends State<NamespacePage> {
onPressed: () => _addListDialog(context), onPressed: () => _addListDialog(context),
child: const Icon(Icons.add))), child: const Icon(Icons.add))),
); );
} }
@override @override

View File

@ -7,7 +7,8 @@ import 'package:vikunja_app/theme/buttonText.dart';
class NamespaceEditPage extends StatefulWidget { class NamespaceEditPage extends StatefulWidget {
final Namespace namespace; final Namespace namespace;
NamespaceEditPage({required this.namespace}) : super(key: Key(namespace.toString())); NamespaceEditPage({required this.namespace})
: super(key: Key(namespace.toString()));
@override @override
State<StatefulWidget> createState() => _NamespaceEditPageState(); State<StatefulWidget> createState() => _NamespaceEditPageState();
@ -63,7 +64,8 @@ class _NamespaceEditPageState extends State<NamespaceEditPage> {
maxLines: null, maxLines: null,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
initialValue: widget.namespace.description, initialValue: widget.namespace.description,
onSaved: (description) => _description = description ?? '', onSaved: (description) =>
_description = description ?? '',
validator: (description) { validator: (description) {
//if (description.length > 1000) { //if (description.length > 1000) {
// return 'The description can have a maximum of 1000 characters.'; // return 'The description can have a maximum of 1000 characters.';

View File

@ -42,36 +42,35 @@ class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
onTap: () => _onSelectItem(i), onTap: () => _onSelectItem(i),
))); )));
if(_selectedDrawerIndex > -1) { if (_selectedDrawerIndex > -1) {
return new WillPopScope( return new WillPopScope(
child: NamespacePage(namespace: _namespaces[_selectedDrawerIndex]), child: NamespacePage(namespace: _namespaces[_selectedDrawerIndex]),
onWillPop: () async {setState(() { onWillPop: () async {
_selectedDrawerIndex = -2; setState(() {
_selectedDrawerIndex = -2;
});
return false;
}); });
return false;});
} }
return Scaffold( return Scaffold(
body: body: this._loading
this._loading ? Center(child: CircularProgressIndicator())
? Center(child: CircularProgressIndicator()) : RefreshIndicator(
: child: ListView(
RefreshIndicator( padding: EdgeInsets.zero,
child: ListView( children: ListTile.divideTiles(
padding: EdgeInsets.zero, context: context, tiles: namespacesList)
children: ListTile.divideTiles( .toList()),
context: context, tiles: namespacesList) onRefresh: _loadNamespaces,
.toList()), ),
onRefresh: _loadNamespaces, floatingActionButton: Builder(
), builder: (context) => FloatingActionButton(
floatingActionButton: Builder( onPressed: () => _addNamespaceDialog(context),
builder: (context) => FloatingActionButton( child: const Icon(Icons.add))),
onPressed: () => _addNamespaceDialog(context), appBar: AppBar(
child: const Icon(Icons.add))),
appBar: AppBar(
title: Text("Namespaces"), title: Text("Namespaces"),
), ),
); );
} }
@ -85,11 +84,13 @@ class _NamespaceOverviewPageState extends State<NamespaceOverviewPage>
} }
_onSelectItem(int index) { _onSelectItem(int index) {
Navigator.push(context, Navigator.push(
context,
MaterialPageRoute( MaterialPageRoute(
builder: (buildContext) => NamespacePage( builder: (buildContext) => NamespacePage(
namespace: _namespaces[index], namespace: _namespaces[index],
),)); ),
));
//setState(() => _selectedDrawerIndex = index); //setState(() => _selectedDrawerIndex = index);
} }

View File

@ -1,4 +1,3 @@
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -38,9 +37,9 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
bool expanded = expandedList.contains(project.id); bool expanded = expandedList.contains(project.id);
Widget icon; Widget icon;
List<Widget>? children = addProjectChildren(project, level+1); List<Widget>? children = addProjectChildren(project, level + 1);
bool no_children = children.length == 0; bool no_children = children.length == 0;
if(no_children) { if (no_children) {
icon = Icon(Icons.list); icon = Icon(Icons.list);
} else { } else {
if (expanded) { if (expanded) {
@ -51,34 +50,34 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
} }
} }
return Column(children: [
return Column(children: [ ListTile(
ListTile( onTap: () {
onTap: () { setState(() {
setState(() { openList(context, project);
openList(context, project); });
}); },
}, contentPadding: insets,
contentPadding: insets,
leading: IconButton( leading: IconButton(
disabledColor: Theme.of(context).unselectedWidgetColor, disabledColor: Theme.of(context).unselectedWidgetColor,
icon: icon, icon: icon,
onPressed: !no_children ? () { onPressed: !no_children
setState(() { ? () {
if (expanded) setState(() {
expandedList.remove(project.id); if (expanded)
else expandedList.remove(project.id);
expandedList.add(project.id); else
}); expandedList.add(project.id);
} : null, });
}
: null,
), ),
title: new Text(project.title), title: new Text(project.title),
//onTap: () => _onSelectItem(i), //onTap: () => _onSelectItem(i),
), ),
...?children ...?children
]); ]);
} }
List<Widget> addProjectChildren(Project project, level) { List<Widget> addProjectChildren(Project project, level) {
Iterable<Project> children = Iterable<Project> children =
@ -121,7 +120,6 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
.toList()), .toList()),
onRefresh: _loadProjects, onRefresh: _loadProjects,
), ),
appBar: AppBar( appBar: AppBar(
title: Text("Projects"), title: Text("Projects"),
), ),
@ -137,8 +135,6 @@ class _ProjectOverviewPageState extends State<ProjectOverviewPage>
}); });
} }
_addProjectDialog(BuildContext context) { _addProjectDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,

View File

@ -0,0 +1 @@

View File

@ -10,7 +10,8 @@ import '../../models/project.dart';
class ProjectEditPage extends StatefulWidget { class ProjectEditPage extends StatefulWidget {
final Project project; final Project project;
ProjectEditPage({required this.project}) : super(key: Key(project.toString())); ProjectEditPage({required this.project})
: super(key: Key(project.toString()));
@override @override
State<StatefulWidget> createState() => _ProjectEditPageState(); State<StatefulWidget> createState() => _ProjectEditPageState();
@ -24,16 +25,18 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
late int listId; late int listId;
@override @override
void initState(){ void initState() {
listId = widget.project.id; listId = widget.project.id;
super.initState(); super.initState();
} }
@override @override
Widget build(BuildContext ctx) { Widget build(BuildContext ctx) {
if(displayDoneTasks == null) if (displayDoneTasks == null)
VikunjaGlobal.of(context).projectService.getDisplayDoneTasks(listId).then( VikunjaGlobal.of(context)
(value) => setState(() => displayDoneTasks = value == "1")); .projectService
.getDisplayDoneTasks(listId)
.then((value) => setState(() => displayDoneTasks = value == "1"));
else else
log("Display done tasks: " + displayDoneTasks.toString()); log("Display done tasks: " + displayDoneTasks.toString());
return Scaffold( return Scaffold(
@ -45,7 +48,7 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
child: Form( child: Form(
key: _formKey, key: _formKey,
child: ListView( child: ListView(
//reverse: true, //reverse: true,
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
children: <Widget>[ children: <Widget>[
Padding( Padding(
@ -73,10 +76,10 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
maxLines: null, maxLines: null,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
initialValue: widget.project.description, initialValue: widget.project.description,
onSaved: (description) => _description = description ?? '', onSaved: (description) =>
_description = description ?? '',
validator: (description) { validator: (description) {
if(description == null) if (description == null) return null;
return null;
if (description.length > 1000) { if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.'; return 'The description can have a maximum of 1000 characters.';
} }
@ -95,7 +98,9 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
title: Text("Show done tasks"), title: Text("Show done tasks"),
onChanged: (value) { onChanged: (value) {
value ??= false; value ??= false;
VikunjaGlobal.of(context).listService.setDisplayDoneTasks(listId, value ? "1" : "0"); VikunjaGlobal.of(context)
.listService
.setDisplayDoneTasks(listId, value ? "1" : "0");
setState(() => displayDoneTasks = value); setState(() => displayDoneTasks = value);
}, },
), ),
@ -106,11 +111,11 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
child: FancyButton( child: FancyButton(
onPressed: !_loading onPressed: !_loading
? () { ? () {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
Form.of(context)?.save(); Form.of(context)?.save();
_saveList(context); _saveList(context);
} }
} }
: () {}, : () {},
child: _loading child: _loading
? CircularProgressIndicator() ? CircularProgressIndicator()
@ -137,8 +142,7 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
},) },)
], ],
)*/ )*/
] ]),
),
), ),
), ),
), ),
@ -149,10 +153,8 @@ class _ProjectEditPageState extends State<ProjectEditPage> {
setState(() => _loading = true); setState(() => _loading = true);
// FIXME: is there a way we can update the list without creating a new list object? // FIXME: is there a way we can update the list without creating a new list object?
// aka updating the existing list we got from context (setters?) // aka updating the existing list we got from context (setters?)
Project newProject = widget.project.copyWith( Project newProject =
title: _title, widget.project.copyWith(title: _title, description: _description);
description: _description
);
VikunjaGlobal.of(context).projectService.update(newProject).then((_) { VikunjaGlobal.of(context).projectService.update(newProject).then((_) {
setState(() => _loading = false); setState(() => _loading = false);
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(

View File

@ -97,42 +97,42 @@ class _ListPageState extends State<ListPage> {
]); ]);
break; break;
case PageStatus.success: case PageStatus.success:
body = taskState.tasks.length > 0 || taskState.buckets.length > 0 || _project.subprojects!.length > 0 body = taskState.tasks.length > 0 ||
taskState.buckets.length > 0 ||
_project.subprojects!.length > 0
? ListenableProvider.value( ? ListenableProvider.value(
value: taskState, value: taskState,
child: Theme( child: Theme(
data: (ThemeData base) { data: (ThemeData base) {
return base.copyWith( return base.copyWith(
chipTheme: base.chipTheme.copyWith( chipTheme: base.chipTheme.copyWith(
labelPadding: EdgeInsets.symmetric(horizontal: 2), labelPadding: EdgeInsets.symmetric(horizontal: 2),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(5)), borderRadius: BorderRadius.all(Radius.circular(5)),
), ),
),
);
}(Theme.of(context)),
child: () {
switch (_viewIndex) {
case 0:
return _listView(context);
case 1:
return _kanban.kanbanView();
default:
return _listView(context);
}
}(),
), ),
); )
}(Theme.of(context)),
child: () {
switch (_viewIndex) {
case 0:
return _listView(context);
case 1:
return _kanban.kanbanView();
default:
return _listView(context);
}
}(),
),
)
: Stack(children: [ : Stack(children: [
ListView(), ListView(),
Center(child: Text('This list is empty.')) Center(child: Text('This list is empty.'))
]); ]);
break; break;
case PageStatus.empty: case PageStatus.empty:
body = new Stack(children: [ body = new Stack(
ListView(), children: [ListView(), Center(child: Text("This view is empty"))]);
Center(child: Text("This view is empty"))
]);
break; break;
} }
@ -156,10 +156,10 @@ class _ListPageState extends State<ListPage> {
floatingActionButton: _viewIndex == 1 floatingActionButton: _viewIndex == 1
? null ? null
: Builder( : Builder(
builder: (context) => FloatingActionButton( builder: (context) => FloatingActionButton(
onPressed: () => _addItemDialog(context), onPressed: () => _addItemDialog(context),
child: Icon(Icons.add)), child: Icon(Icons.add)),
), ),
bottomNavigationBar: BottomNavigationBar( bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[ items: const <BottomNavigationBarItem>[
BottomNavigationBarItem( BottomNavigationBarItem(
@ -183,22 +183,23 @@ class _ListPageState extends State<ListPage> {
return Container( return Container(
height: 80, height: 80,
padding: EdgeInsets.fromLTRB(10, 0, 10, 0), padding: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: child: ListView(
ListView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
//mainAxisAlignment: MainAxisAlignment.spaceEvenly, //mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [ children: [
...?widget.project.subprojects?.map((elem) => ...?widget.project.subprojects?.map((elem) => InkWell(
InkWell( onTap: () {
onTap: () {openList(context, elem);}, openList(context, elem);
child: },
Container( child: Container(
alignment: Alignment.center, alignment: Alignment.center,
height: 20, height: 20,
width: 100, width: 100,
child: child: Text(
Text(elem.title, overflow: TextOverflow.ellipsis,softWrap: false,))) elem.title,
), overflow: TextOverflow.ellipsis,
softWrap: false,
)))),
], ],
), ),
); );
@ -215,39 +216,50 @@ class _ListPageState extends State<ListPage> {
Widget _listView(BuildContext context) { Widget _listView(BuildContext context) {
List<Widget> children = []; List<Widget> children = [];
if(widget.project.subprojects?.length != 0) { if (widget.project.subprojects?.length != 0) {
children.add(Padding(child: Text("Projects", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); children.add(Padding(
child: Text(
"Projects",
style: TextStyle(fontWeight: FontWeight.bold),
),
padding: EdgeInsets.fromLTRB(0, 10, 0, 0),
));
children.add(buildSubProjectSelector()); children.add(buildSubProjectSelector());
} }
if(taskState.tasks.length != 0) { if (taskState.tasks.length != 0) {
children.add(Padding(child: Text("Tasks", style: TextStyle(fontWeight: FontWeight.bold),), padding: EdgeInsets.fromLTRB(0, 10, 0, 0),)); children.add(Padding(
child: Text(
"Tasks",
style: TextStyle(fontWeight: FontWeight.bold),
),
padding: EdgeInsets.fromLTRB(0, 10, 0, 0),
));
children.add(Divider()); children.add(Divider());
children.add(Expanded(child: children.add(Expanded(
ListView.builder( child: ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0), padding: EdgeInsets.symmetric(vertical: 8.0),
itemCount: taskState.tasks.length * 2, itemCount: taskState.tasks.length * 2,
itemBuilder: (context, i) { itemBuilder: (context, i) {
if (i.isOdd) return Divider(); if (i.isOdd) return Divider();
if (_loadingTasks.isNotEmpty) { if (_loadingTasks.isNotEmpty) {
final loadingTask = _loadingTasks.removeLast(); final loadingTask = _loadingTasks.removeLast();
return _buildLoadingTile(loadingTask); return _buildLoadingTile(loadingTask);
} }
final index = i ~/ 2; final index = i ~/ 2;
if (taskState.maxPages == _currentPage && if (taskState.maxPages == _currentPage &&
index == taskState.tasks.length) index == taskState.tasks.length)
throw Exception("Check itemCount attribute"); throw Exception("Check itemCount attribute");
if (index >= taskState.tasks.length && if (index >= taskState.tasks.length &&
_currentPage < taskState.maxPages) { _currentPage < taskState.maxPages) {
_currentPage++; _currentPage++;
_loadTasksForPage(_currentPage); _loadTasksForPage(_currentPage);
} }
return _buildTile(taskState.tasks[index]); return _buildTile(taskState.tasks[index]);
}))); })));
} }
return Column(children: children); return Column(children: children);
@ -304,13 +316,11 @@ class _ListPageState extends State<ListPage> {
_loadTasksForPage(1); _loadTasksForPage(1);
break; break;
case 1: case 1:
await _kanban await _kanban.loadBucketsForPage(1);
.loadBucketsForPage(1);
// load all buckets to get length for RecordableListView // load all buckets to get length for RecordableListView
while (_currentPage < taskState.maxPages) { while (_currentPage < taskState.maxPages) {
_currentPage++; _currentPage++;
await _kanban await _kanban.loadBucketsForPage(_currentPage);
.loadBucketsForPage(_currentPage);
} }
break; break;
default: default:
@ -320,8 +330,7 @@ class _ListPageState extends State<ListPage> {
} }
Future<void> _loadTasksForPage(int page) { Future<void> _loadTasksForPage(int page) {
return Provider.of<ProjectProvider>(context, listen: false) return Provider.of<ProjectProvider>(context, listen: false).loadTasks(
.loadTasks(
context: context, context: context,
listId: _project.id, listId: _project.id,
page: page, page: page,
@ -335,7 +344,7 @@ class _ListPageState extends State<ListPage> {
onAdd: (title) => _addItem(title, context, bucket), onAdd: (title) => _addItem(title, context, bucket),
decoration: InputDecoration( decoration: InputDecoration(
labelText: labelText:
(bucket != null ? '\'${bucket.title}\': ' : '') + 'New Task Name', (bucket != null ? '\'${bucket.title}\': ' : '') + 'New Task Name',
hintText: 'eg. Milk', hintText: 'eg. Milk',
), ),
), ),
@ -377,7 +386,6 @@ class _ListPageState extends State<ListPage> {
} }
} }
openList(BuildContext context, Project project) { openList(BuildContext context, Project project) {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<ProjectProvider>( builder: (context) => ChangeNotifierProvider<ProjectProvider>(
@ -388,4 +396,4 @@ openList(BuildContext context, Project project) {
), ),
// ListPage(taskList: list) // ListPage(taskList: list)
)); ));
} }

View File

@ -10,7 +10,6 @@ import '../models/project.dart';
import '../models/user.dart'; import '../models/user.dart';
import '../service/services.dart'; import '../service/services.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
@override @override
State<StatefulWidget> createState() => SettingsPageState(); State<StatefulWidget> createState() => SettingsPageState();
@ -27,7 +26,6 @@ class SettingsPageState extends State<SettingsPage> {
FlutterThemeMode? themeMode; FlutterThemeMode? themeMode;
User? currentUser; User? currentUser;
void init() { void init() {
durationTextController = TextEditingController(); durationTextController = TextEditingController();
@ -53,20 +51,21 @@ class SettingsPageState extends State<SettingsPage> {
.getCurrentVersionTag() .getCurrentVersionTag()
.then((value) => setState(() => versionTag = value)); .then((value) => setState(() => versionTag = value));
VikunjaGlobal.of(context).settingsManager.getWorkmanagerDuration().then(
(value) => setState(
() => durationTextController.text = (value.inMinutes.toString())));
VikunjaGlobal.of(context) VikunjaGlobal.of(context)
.settingsManager .settingsManager
.getWorkmanagerDuration() .getThemeMode()
.then((value) => setState(() => durationTextController.text = (value.inMinutes.toString()))); .then((value) => setState(() => themeMode = value));
VikunjaGlobal.of(context).settingsManager.getThemeMode().then((value) => setState(() => themeMode = value));
VikunjaGlobal.of(context).newUserService?.getCurrentUser().then((value) => { VikunjaGlobal.of(context).newUserService?.getCurrentUser().then((value) => {
setState(() { setState(() {
currentUser = value!; currentUser = value!;
defaultProject = value.settings?.default_project_id; defaultProject = value.settings?.default_project_id;
} ), }),
}); });
initialized = true; initialized = true;
} }
@ -77,18 +76,22 @@ class SettingsPageState extends State<SettingsPage> {
if (!initialized) init(); if (!initialized) init();
return new Scaffold( return new Scaffold(
appBar: AppBar(title: Text("Settings"),), appBar: AppBar(
title: Text("Settings"),
),
body: ListView( body: ListView(
children: [ children: [
UserAccountsDrawerHeader( UserAccountsDrawerHeader(
accountName: currentUser != null ? Text(currentUser!.username) : null, accountName:
currentUser != null ? Text(currentUser!.username) : null,
accountEmail: currentUser != null ? Text(currentUser!.name) : null, accountEmail: currentUser != null ? Text(currentUser!.name) : null,
currentAccountPicture: currentUser == null currentAccountPicture: currentUser == null
? null ? null
: CircleAvatar( : CircleAvatar(
backgroundImage: (currentUser?.username != "") ? NetworkImage(currentUser!.avatarUrl(context)) : null, backgroundImage: (currentUser?.username != "")
), ? NetworkImage(currentUser!.avatarUrl(context))
: null,
),
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"), image: AssetImage("assets/graphics/hypnotize.png"),
@ -111,11 +114,17 @@ class SettingsPageState extends State<SettingsPage> {
child: Text(e.title), value: e.id)) child: Text(e.title), value: e.id))
.toList() .toList()
], ],
value: projectList?.firstWhereOrNull((element) => element.id == defaultProject) != null ? defaultProject : null, value: projectList?.firstWhereOrNull(
(element) => element.id == defaultProject) !=
null
? defaultProject
: null,
onChanged: (int? value) { onChanged: (int? value) {
setState(() => defaultProject = value); setState(() => defaultProject = value);
global.newUserService?.setCurrentUserSettings( global.newUserService
currentUser!.settings!.copyWith(default_project_id: value)).then((value) => currentUser!.settings = value); ?.setCurrentUserSettings(currentUser!.settings!
.copyWith(default_project_id: value))
.then((value) => currentUser!.settings = value);
//VikunjaGlobal.of(context).userManager.setDefaultList(value); //VikunjaGlobal.of(context).userManager.setDefaultList(value);
}, },
), ),
@ -151,9 +160,7 @@ class SettingsPageState extends State<SettingsPage> {
], ],
value: themeMode, value: themeMode,
onChanged: (FlutterThemeMode? value) { onChanged: (FlutterThemeMode? value) {
VikunjaGlobal.of(context) VikunjaGlobal.of(context).settingsManager.setThemeMode(value!);
.settingsManager
.setThemeMode(value!);
setState(() => themeMode = value); setState(() => themeMode = value);
updateTheme.value = true; updateTheme.value = true;
}, },
@ -169,24 +176,27 @@ class SettingsPageState extends State<SettingsPage> {
VikunjaGlobal.of(context).client.reload_ignore_certs(value); VikunjaGlobal.of(context).client.reload_ignore_certs(value);
}) })
: ListTile(title: Text("...")), : ListTile(title: Text("...")),
Divider(), Divider(),
Padding(padding: EdgeInsets.only(left: 15, right: 15), Padding(
padding: EdgeInsets.only(left: 15, right: 15),
child: Row(children: [ child: Row(children: [
Flexible( Flexible(
child: TextField( child: TextField(
controller: durationTextController, controller: durationTextController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Background Refresh Interval (minutes): ', labelText: 'Background Refresh Interval (minutes): ',
helperText: 'Minimum: 15, Set limit of 0 for no refresh', helperText: 'Minimum: 15, Set limit of 0 for no refresh',
), ),
)), )),
TextButton( TextButton(
onPressed: () => VikunjaGlobal.of(context) onPressed: () => VikunjaGlobal.of(context)
.settingsManager .settingsManager
.setWorkmanagerDuration(Duration( .setWorkmanagerDuration(Duration(
minutes: int.parse(durationTextController.text))).then((value) => VikunjaGlobal.of(context).updateWorkmanagerDuration()), minutes: int.parse(durationTextController.text)))
child: Text("Save")), .then((value) => VikunjaGlobal.of(context)
])), .updateWorkmanagerDuration()),
child: Text("Save")),
])),
Divider(), Divider(),
getVersionNotifications != null getVersionNotifications != null
? CheckboxListTile( ? CheckboxListTile(

View File

@ -33,11 +33,10 @@ class _LoginPageState extends State<LoginPage> {
final _serverSuggestionController = SuggestionsController(); final _serverSuggestionController = SuggestionsController();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Future.delayed(Duration.zero, () async{ Future.delayed(Duration.zero, () async {
if (VikunjaGlobal.of(context).expired) { if (VikunjaGlobal.of(context).expired) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Login has expired. Please reenter your details!"))); content: Text("Login has expired. Please reenter your details!")));
@ -48,10 +47,16 @@ class _LoginPageState extends State<LoginPage> {
}); });
} }
final client = VikunjaGlobal.of(context).client; final client = VikunjaGlobal.of(context).client;
await VikunjaGlobal.of(context).settingsManager.getIgnoreCertificates().then( await VikunjaGlobal.of(context)
(value) => setState(() => client.ignoreCertificates = value == "1")); .settingsManager
.getIgnoreCertificates()
.then((value) =>
setState(() => client.ignoreCertificates = value == "1"));
await VikunjaGlobal.of(context).settingsManager.getPastServers().then((value) { await VikunjaGlobal.of(context)
.settingsManager
.getPastServers()
.then((value) {
print(value); print(value);
if (value != null) setState(() => pastServers = value); if (value != null) setState(() => pastServers = value);
}); });
@ -101,11 +106,11 @@ class _LoginPageState extends State<LoginPage> {
enabled: !_loading, enabled: !_loading,
validator: (address) { validator: (address) {
return (isUrl(address) || return (isUrl(address) ||
address != null || address != null ||
address!.isEmpty) address!.isEmpty)
? null ? null
: 'Invalid URL'; : 'Invalid URL';
}, },
decoration: new InputDecoration( decoration: new InputDecoration(
border: OutlineInputBorder(), border: OutlineInputBorder(),
labelText: 'Server Address'), labelText: 'Server Address'),
@ -120,34 +125,42 @@ class _LoginPageState extends State<LoginPage> {
),*/ ),*/
onSelected: (suggestion) { onSelected: (suggestion) {
_serverController.text = suggestion; _serverController.text = suggestion;
setState(() => _serverController.text = suggestion); setState(
() => _serverController.text = suggestion);
}, },
itemBuilder: (BuildContext context, Object? itemData) { itemBuilder:
(BuildContext context, Object? itemData) {
return Card( return Card(
child: Container( child: Container(
padding: EdgeInsets.all(10), padding: EdgeInsets.all(10),
child: child: Row(
Row( mainAxisAlignment:
mainAxisAlignment: MainAxisAlignment.spaceBetween, MainAxisAlignment.spaceBetween,
children: [ children: [
Text(itemData.toString()), Text(itemData.toString()),
IconButton(onPressed: () { IconButton(
setState(() { onPressed: () {
pastServers.remove(itemData.toString()); setState(() {
//_serverSuggestionController.suggestionsBox?.close(); pastServers.remove(
VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers); itemData.toString());
//_serverSuggestionController.suggestionsBox?.close();
}); VikunjaGlobal.of(context)
}, icon: Icon(Icons.clear)) .settingsManager
], .setPastServers(
)) pastServers);
); });
},
icon: Icon(Icons.clear))
],
)));
}, },
suggestionsCallback: (String pattern) { suggestionsCallback: (String pattern) {
List<String> matches = <String>[]; List<String> matches = <String>[];
matches.addAll(pastServers); matches.addAll(pastServers);
matches.retainWhere((s){ matches.retainWhere((s) {
return s.toLowerCase().contains(pattern.toLowerCase()); return s
.toLowerCase()
.contains(pattern.toLowerCase());
}); });
return matches; return matches;
}, },
@ -246,20 +259,18 @@ class _LoginPageState extends State<LoginPage> {
} }
}, },
child: VikunjaButtonText("Login with Frontend"))), child: VikunjaButtonText("Login with Frontend"))),
CheckboxListTile( CheckboxListTile(
title: Text("Ignore Certificates"), title: Text("Ignore Certificates"),
value: client.ignoreCertificates, value: client.ignoreCertificates,
onChanged: (value) { onChanged: (value) {
setState(() => setState(
client.reload_ignore_certs(value ?? false)); () => client.reload_ignore_certs(value ?? false));
VikunjaGlobal.of(context) VikunjaGlobal.of(context)
.settingsManager .settingsManager
.setIgnoreCertificates(value ?? false); .setIgnoreCertificates(value ?? false);
VikunjaGlobal.of(context) VikunjaGlobal.of(context).client.ignoreCertificates =
.client value ?? false;
.ignoreCertificates = value ?? false; })
})
], ],
), ),
), ),
@ -276,7 +287,7 @@ class _LoginPageState extends State<LoginPage> {
String _password = _passwordController.text; String _password = _passwordController.text;
if (_server.isEmpty) return; if (_server.isEmpty) return;
if(!pastServers.contains(_server)) pastServers.add(_server); if (!pastServers.contains(_server)) pastServers.add(_server);
await VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers); await VikunjaGlobal.of(context).settingsManager.setPastServers(pastServers);
setState(() => _loading = true); setState(() => _loading = true);
@ -285,8 +296,7 @@ class _LoginPageState extends State<LoginPage> {
vGlobal.client.showSnackBar = false; vGlobal.client.showSnackBar = false;
vGlobal.client.configure(base: _server); vGlobal.client.configure(base: _server);
Server? info = await vGlobal.serverService.getInfo(); Server? info = await vGlobal.serverService.getInfo();
if (info == null) if (info == null) throw Exception("Getting server info failed");
throw Exception("Getting server info failed");
UserTokenPair newUser; UserTokenPair newUser;
@ -297,27 +307,27 @@ class _LoginPageState extends State<LoginPage> {
TextEditingController totpController = TextEditingController(); TextEditingController totpController = TextEditingController();
bool dismissed = true; bool dismissed = true;
await showDialog( await showDialog(
context: context, context: context,
builder: (context) => new AlertDialog( builder: (context) => new AlertDialog(
title: Text("Enter One Time Passcode"), title: Text("Enter One Time Passcode"),
content: TextField( content: TextField(
controller: totpController, controller: totpController,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: <TextInputFormatter>[ inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly FilteringTextInputFormatter.digitsOnly
], ],
), ),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
dismissed = false; dismissed = false;
Navigator.pop(context); Navigator.pop(context);
}, },
child: Text("Login")) child: Text("Login"))
], ],
), ),
); );
if(!dismissed) { if (!dismissed) {
newUser = await vGlobal.newUserService!.login(_username, _password, newUser = await vGlobal.newUserService!.login(_username, _password,
rememberMe: this._rememberMe, totp: totpController.text); rememberMe: this._rememberMe, totp: totpController.text);
} else { } else {

View File

@ -16,27 +16,25 @@ class LoginWithWebView extends StatefulWidget {
} }
class LoginWithWebViewState extends State<LoginWithWebView> { class LoginWithWebViewState extends State<LoginWithWebView> {
WebViewWidget? webView; WebViewWidget? webView;
late WebViewController webViewController; late WebViewController webViewController;
bool destroyed = false; bool destroyed = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
webViewController = WebViewController() webViewController = WebViewController()
..clearLocalStorage() ..clearLocalStorage()
..setJavaScriptMode(JavaScriptMode.unrestricted) ..setJavaScriptMode(JavaScriptMode.unrestricted)
..setUserAgent("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") ..setUserAgent(
"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")
..setNavigationDelegate(NavigationDelegate( ..setNavigationDelegate(NavigationDelegate(
onPageFinished: (value) => _handlePageFinished(value), onPageFinished: (value) => _handlePageFinished(value),
)) ))
..loadRequest(Uri.parse(widget.frontEndUrl)).then((value) => { ..loadRequest(Uri.parse(widget.frontEndUrl)).then((value) => {
webViewController!.runJavaScript("localStorage.clear(); location.href=location.href;") webViewController!.runJavaScript(
}); "localStorage.clear(); location.href=location.href;")
});
/* /*
webView = WebViewWidget( webView = WebViewWidget(
@ -50,47 +48,52 @@ class LoginWithWebViewState extends State<LoginWithWebView> {
}, },
); );
*/ */
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope(child: Scaffold( return WillPopScope(
appBar: AppBar(), child: Scaffold(
body: WebViewWidget(controller: webViewController,) appBar: AppBar(),
), body: WebViewWidget(
onWillPop: () async { controller: webViewController,
String? currentUrl = await webViewController?.currentUrl(); )),
if (currentUrl != null) { onWillPop: () async {
bool hasPopped = await _handlePageFinished(currentUrl); String? currentUrl = await webViewController?.currentUrl();
return Future.value(!hasPopped); if (currentUrl != null) {
} bool hasPopped = await _handlePageFinished(currentUrl);
return Future.value(false); return Future.value(!hasPopped);
},); }
return Future.value(false);
},
);
} }
Future<bool> _handlePageFinished(String pageLocation) async { Future<bool> _handlePageFinished(String pageLocation) async {
log("handlePageFinished"); log("handlePageFinished");
if(webViewController != null) { if (webViewController != null) {
String localStorage = (await webViewController! String localStorage = (await webViewController!
.runJavaScriptReturningResult("JSON.stringify(localStorage);")).toString(); .runJavaScriptReturningResult("JSON.stringify(localStorage);"))
.toString();
String apiUrl = (await webViewController!.runJavaScriptReturningResult("API_URL")).toString(); String apiUrl =
String token = (await webViewController!.runJavaScriptReturningResult("localStorage['token']")).toString(); (await webViewController!.runJavaScriptReturningResult("API_URL"))
.toString();
String token = (await webViewController!
.runJavaScriptReturningResult("localStorage['token']"))
.toString();
if (localStorage.toString() != "{}") { if (localStorage.toString() != "{}") {
apiUrl = apiUrl.replaceAll("\"", ""); apiUrl = apiUrl.replaceAll("\"", "");
token = token.replaceAll("\"", ""); token = token.replaceAll("\"", "");
if(!apiUrl.startsWith("http")) { if (!apiUrl.startsWith("http")) {
if(pageLocation.endsWith("/")) if (pageLocation.endsWith("/"))
pageLocation = pageLocation.substring(0,pageLocation.length-1); pageLocation = pageLocation.substring(0, pageLocation.length - 1);
apiUrl = pageLocation + apiUrl; apiUrl = pageLocation + apiUrl;
} }
if (apiUrl != "null" && token != "null") { if (apiUrl != "null" && token != "null") {
BaseTokenPair baseTokenPair = BaseTokenPair( BaseTokenPair baseTokenPair = BaseTokenPair(apiUrl, token);
apiUrl, token); if (destroyed) return true;
if(destroyed)
return true;
destroyed = true; destroyed = true;
print("pop now"); print("pop now");
Navigator.pop(context, baseTokenPair); Navigator.pop(context, baseTokenPair);
@ -101,5 +104,4 @@ class LoginWithWebViewState extends State<LoginWithWebView> {
} }
return false; return false;
} }
}
}

View File

@ -127,10 +127,9 @@ class _RegisterPageState extends State<RegisterPage> {
setState(() => _loading = true); setState(() => _loading = true);
try { try {
var vGlobal = VikunjaGlobal.of(context); var vGlobal = VikunjaGlobal.of(context);
var newUserLoggedIn = await vGlobal var newUserLoggedIn =
.newUserService await vGlobal.newUserService?.register(_username!, _email, _password);
?.register(_username!, _email, _password); if (newUserLoggedIn != null)
if(newUserLoggedIn != null)
vGlobal.changeUser(newUserLoggedIn.user!, vGlobal.changeUser(newUserLoggedIn.user!,
token: newUserLoggedIn.token, base: _server!); token: newUserLoggedIn.token, base: _server!);
} catch (ex) { } catch (ex) {

View File

@ -190,7 +190,8 @@ class MockedTaskService implements TaskService {
} }
@override @override
Future<Response?> getAllByProject(int projectId, [Map<String, List<String>>? queryParameters]) { Future<Response?> getAllByProject(int projectId,
[Map<String, List<String>>? queryParameters]) {
// TODO: implement getAllByProject // TODO: implement getAllByProject
return Future.value(new Response(_tasks.values.toList(), 200, {})); return Future.value(new Response(_tasks.values.toList(), 200, {}));
} }
@ -198,7 +199,8 @@ class MockedTaskService implements TaskService {
class MockedUserService implements UserService { class MockedUserService implements UserService {
@override @override
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String? totp}) { Future<UserTokenPair> login(String username, password,
{bool rememberMe = false, String? totp}) {
return Future.value(UserTokenPair(_users[1]!, 'abcdefg')); return Future.value(UserTokenPair(_users[1]!, 'abcdefg'));
} }
@ -223,6 +225,4 @@ class MockedUserService implements UserService {
// TODO: implement getToken // TODO: implement getToken
throw UnimplementedError(); throw UnimplementedError();
} }
} }

View File

@ -60,9 +60,9 @@ class TaskServiceOption<T> {
dynamic defValue; dynamic defValue;
TaskServiceOption(this.name, dynamic input_values) { TaskServiceOption(this.name, dynamic input_values) {
if(input_values is List<String>) { if (input_values is List<String>) {
valueList = input_values; valueList = input_values;
} else if(input_values is String) { } else if (input_values is String) {
value = input_values; value = input_values;
} }
} }
@ -82,20 +82,14 @@ class TaskServiceOption<T> {
final List<TaskServiceOption> defaultOptions = [ final List<TaskServiceOption> defaultOptions = [
TaskServiceOption<TaskServiceOptionSortBy>("sort_by", TaskServiceOption<TaskServiceOptionSortBy>("sort_by",
[TaskServiceOptionSortBy.due_date, [TaskServiceOptionSortBy.due_date, TaskServiceOptionSortBy.id]),
TaskServiceOptionSortBy.id]),
TaskServiceOption<TaskServiceOptionOrderBy>( TaskServiceOption<TaskServiceOptionOrderBy>(
"order_by", TaskServiceOptionOrderBy.asc), "order_by", TaskServiceOptionOrderBy.asc),
TaskServiceOption<TaskServiceOptionFilterBy>("filter_by", [ TaskServiceOption<TaskServiceOptionFilterBy>("filter_by",
TaskServiceOptionFilterBy.done, [TaskServiceOptionFilterBy.done, TaskServiceOptionFilterBy.due_date]),
TaskServiceOptionFilterBy.due_date TaskServiceOption<TaskServiceOptionFilterValue>("filter_value",
]), [TaskServiceOptionFilterValue.enum_false, '1970-01-01T00:00:00.000Z']),
TaskServiceOption<TaskServiceOptionFilterValue>("filter_value", [ TaskServiceOption<TaskServiceOptionFilterComparator>("filter_comparator", [
TaskServiceOptionFilterValue.enum_false,
'1970-01-01T00:00:00.000Z'
]),
TaskServiceOption<TaskServiceOptionFilterComparator>(
"filter_comparator", [
TaskServiceOptionFilterComparator.equals, TaskServiceOptionFilterComparator.equals,
TaskServiceOptionFilterComparator.greater TaskServiceOptionFilterComparator.greater
]), ]),
@ -106,13 +100,14 @@ final List<TaskServiceOption> defaultOptions = [
class TaskServiceOptions { class TaskServiceOptions {
List<TaskServiceOption> options = []; List<TaskServiceOption> options = [];
TaskServiceOptions({List<TaskServiceOption>? newOptions, bool clearOther = false}) { TaskServiceOptions(
if(!clearOther) {List<TaskServiceOption>? newOptions, bool clearOther = false}) {
options = new List<TaskServiceOption>.from(defaultOptions); if (!clearOther) options = new List<TaskServiceOption>.from(defaultOptions);
if (newOptions != null) { if (newOptions != null) {
for (TaskServiceOption custom_option in newOptions) { for (TaskServiceOption custom_option in newOptions) {
int index = options.indexWhere((element) => element.name == custom_option.name); int index =
if(index > -1) { options.indexWhere((element) => element.name == custom_option.name);
if (index > -1) {
options.removeAt(index); options.removeAt(index);
} else { } else {
index = options.length; index = options.length;
@ -122,13 +117,12 @@ class TaskServiceOptions {
} }
} }
Map<String, List<String>> getOptions() { Map<String, List<String>> getOptions() {
Map<String, List<String>> queryparams = {}; Map<String, List<String>> queryparams = {};
for (TaskServiceOption option in options) { for (TaskServiceOption option in options) {
dynamic value = option.getValue(); dynamic value = option.getValue();
if (value is List) { if (value is List) {
queryparams[option.name+"[]"] = value as List<String>; queryparams[option.name + "[]"] = value as List<String>;
//for (dynamic valueEntry in value) { //for (dynamic valueEntry in value) {
// result += '&' + option.name + '[]=' + valueEntry; // result += '&' + option.name + '[]=' + valueEntry;
//} //}
@ -152,14 +146,12 @@ abstract class ProjectService {
Future<Project?> update(Project p); Future<Project?> update(Project p);
Future delete(int projectId); Future delete(int projectId);
Future<String?> getDisplayDoneTasks(int listId); Future<String?> getDisplayDoneTasks(int listId);
void setDisplayDoneTasks(int listId, String value); void setDisplayDoneTasks(int listId, String value);
//Future<String?> getDefaultList(); //Future<String?> getDefaultList();
//void setDefaultList(int? listId); //void setDefaultList(int? listId);
} }
abstract class NamespaceService { abstract class NamespaceService {
Future<List<Namespace>?> getAll(); Future<List<Namespace>?> getAll();
@ -290,7 +282,6 @@ class SettingsManager {
}); });
} }
SettingsManager(this._storage) { SettingsManager(this._storage) {
applydefaults(); applydefaults();
} }
@ -298,35 +289,45 @@ class SettingsManager {
Future<String?> getIgnoreCertificates() { Future<String?> getIgnoreCertificates() {
return _storage.read(key: "ignore-certificates"); return _storage.read(key: "ignore-certificates");
} }
void setIgnoreCertificates(bool value) { void setIgnoreCertificates(bool value) {
_storage.write(key: "ignore-certificates", value: value ? "1" : "0"); _storage.write(key: "ignore-certificates", value: value ? "1" : "0");
} }
Future<bool> getLandingPageOnlyDueDateTasks() { Future<bool> getLandingPageOnlyDueDateTasks() {
return _storage.read(key: "landing-page-due-date-tasks").then((value) => value == "1"); return _storage
} .read(key: "landing-page-due-date-tasks")
Future<void> setLandingPageOnlyDueDateTasks(bool value) { .then((value) => value == "1");
return _storage.write(key: "landing-page-due-date-tasks", value: value ? "1" : "0");
} }
Future<void> setLandingPageOnlyDueDateTasks(bool value) {
return _storage.write(
key: "landing-page-due-date-tasks", value: value ? "1" : "0");
}
Future<String?> getVersionNotifications() { Future<String?> getVersionNotifications() {
return _storage.read(key: "get-version-notifications"); return _storage.read(key: "get-version-notifications");
} }
void setVersionNotifications(bool value) { void setVersionNotifications(bool value) {
_storage.write(key: "get-version-notifications", value: value ? "1" : "0"); _storage.write(key: "get-version-notifications", value: value ? "1" : "0");
} }
Future<Duration> getWorkmanagerDuration() { Future<Duration> getWorkmanagerDuration() {
return _storage.read(key: "workmanager-duration").then((value) => Duration(minutes: int.parse(value ?? "0"))); return _storage
.read(key: "workmanager-duration")
.then((value) => Duration(minutes: int.parse(value ?? "0")));
} }
Future<void> setWorkmanagerDuration(Duration duration) { Future<void> setWorkmanagerDuration(Duration duration) {
return _storage.write(key: "workmanager-duration", value: duration.inMinutes.toString()); return _storage.write(
key: "workmanager-duration", value: duration.inMinutes.toString());
} }
Future<List<String>?> getPastServers() { Future<List<String>?> getPastServers() {
return _storage.read(key: "recent-servers").then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>()); return _storage
.read(key: "recent-servers")
.then((value) => (jsonDecode(value!) as List<dynamic>).cast<String>());
} }
Future<void> setPastServers(List<String>? server) { Future<void> setPastServers(List<String>? server) {
@ -337,18 +338,17 @@ class SettingsManager {
Future<FlutterThemeMode> getThemeMode() async { Future<FlutterThemeMode> getThemeMode() async {
String? theme_mode = await _storage.read(key: "theme_mode"); String? theme_mode = await _storage.read(key: "theme_mode");
if(theme_mode == null) if (theme_mode == null) setThemeMode(FlutterThemeMode.system);
setThemeMode(FlutterThemeMode.system); switch (theme_mode) {
switch(theme_mode) {
case "system": case "system":
return FlutterThemeMode.system; return FlutterThemeMode.system;
case "light": case "light":
return FlutterThemeMode.light; return FlutterThemeMode.light;
case "dark": case "dark":
return FlutterThemeMode.dark; return FlutterThemeMode.dark;
case "materialYouLight": case "materialYouLight":
return FlutterThemeMode.materialYouLight; return FlutterThemeMode.materialYouLight;
case "materialYouDark": case "materialYouDark":
return FlutterThemeMode.materialYouDark; return FlutterThemeMode.materialYouDark;
default: default:
return FlutterThemeMode.system; return FlutterThemeMode.system;
@ -356,9 +356,9 @@ class SettingsManager {
} }
Future<void> setThemeMode(FlutterThemeMode newMode) async { Future<void> setThemeMode(FlutterThemeMode newMode) async {
await _storage.write(key: "theme_mode", value: newMode.toString().split('.').last); await _storage.write(
key: "theme_mode", value: newMode.toString().split('.').last);
} }
} }
enum FlutterThemeMode { enum FlutterThemeMode {
@ -367,4 +367,4 @@ enum FlutterThemeMode {
dark, dark,
materialYouLight, materialYouLight,
materialYouDark, materialYouDark,
} }

View File

@ -1,4 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/bucket.dart'; import 'package:vikunja_app/models/bucket.dart';
@ -15,7 +14,6 @@ class ListProvider with ChangeNotifier {
List<Task> _tasks = []; List<Task> _tasks = [];
List<Bucket> _buckets = []; List<Bucket> _buckets = [];
bool get taskDragging => _taskDragging; bool get taskDragging => _taskDragging;
set taskDragging(bool value) { set taskDragging(bool value) {
@ -39,7 +37,6 @@ class ListProvider with ChangeNotifier {
List<Bucket> get buckets => _buckets; List<Bucket> get buckets => _buckets;
PageStatus _pageStatus = PageStatus.built; PageStatus _pageStatus = PageStatus.built;
PageStatus get pageStatus => _pageStatus; PageStatus get pageStatus => _pageStatus;
@ -50,7 +47,11 @@ class ListProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> loadTasks({required BuildContext context, required int listId, int page = 1, bool displayDoneTasks = true}) { Future<void> loadTasks(
{required BuildContext context,
required int listId,
int page = 1,
bool displayDoneTasks = true}) {
_tasks = []; _tasks = [];
notifyListeners(); notifyListeners();
@ -60,7 +61,7 @@ class ListProvider with ChangeNotifier {
"page": [page.toString()] "page": [page.toString()]
}; };
if(!displayDoneTasks) { if (!displayDoneTasks) {
queryParams.addAll({ queryParams.addAll({
"filter_by": ["done"], "filter_by": ["done"],
"filter_value": ["false"] "filter_value": ["false"]
@ -81,7 +82,8 @@ class ListProvider with ChangeNotifier {
});*/ });*/
} }
Future<void> loadBuckets({required BuildContext context, required int listId, int page = 1}) { Future<void> loadBuckets(
{required BuildContext context, required int listId, int page = 1}) {
_buckets = []; _buckets = [];
pageStatus = PageStatus.loading; pageStatus = PageStatus.loading;
notifyListeners(); notifyListeners();
@ -90,8 +92,11 @@ class ListProvider with ChangeNotifier {
"page": [page.toString()] "page": [page.toString()]
}; };
return VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) { return VikunjaGlobal.of(context)
if(response == null) { .bucketService
.getAllByList(listId, queryParams)
.then((response) {
if (response == null) {
pageStatus = PageStatus.error; pageStatus = PageStatus.error;
return; return;
} }
@ -105,7 +110,9 @@ class ListProvider with ChangeNotifier {
} }
Future<void> addTaskByTitle( Future<void> addTaskByTitle(
{required BuildContext context, required String title, required int listId}) async{ {required BuildContext context,
required String title,
required int listId}) async {
final globalState = VikunjaGlobal.of(context); final globalState = VikunjaGlobal.of(context);
if (globalState.currentUser == null) { if (globalState.currentUser == null) {
return; return;
@ -120,13 +127,15 @@ class ListProvider with ChangeNotifier {
pageStatus = PageStatus.loading; pageStatus = PageStatus.loading;
return globalState.taskService.add(listId, newTask).then((task) { return globalState.taskService.add(listId, newTask).then((task) {
if(task != null) if (task != null) _tasks.insert(0, task);
_tasks.insert(0, task);
pageStatus = PageStatus.success; pageStatus = PageStatus.success;
}); });
} }
Future<void> addTask({required BuildContext context, required Task newTask, required int listId}) { Future<void> addTask(
{required BuildContext context,
required Task newTask,
required int listId}) {
var globalState = VikunjaGlobal.of(context); var globalState = VikunjaGlobal.of(context);
if (newTask.bucketId == null) pageStatus = PageStatus.loading; if (newTask.bucketId == null) pageStatus = PageStatus.loading;
notifyListeners(); notifyListeners();
@ -136,107 +145,129 @@ class ListProvider with ChangeNotifier {
pageStatus = PageStatus.error; pageStatus = PageStatus.error;
return; return;
} }
if (_tasks.isNotEmpty) if (_tasks.isNotEmpty) _tasks.insert(0, task);
_tasks.insert(0, task);
if (_buckets.isNotEmpty) { if (_buckets.isNotEmpty) {
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)]; final bucket =
_buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
bucket.tasks.add(task); bucket.tasks.add(task);
} }
pageStatus = PageStatus.success; pageStatus = PageStatus.success;
}); });
} }
Future<Task?> updateTask({required BuildContext context, required Task task}) { Future<Task?> updateTask(
{required BuildContext context, required Task task}) {
return VikunjaGlobal.of(context).taskService.update(task).then((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. // 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 definitely fix it later. // This is enough for now (it works) but we should definitely fix it later.
if(task == null) if (task == null) return null;
return null;
_tasks.asMap().forEach((i, t) { _tasks.asMap().forEach((i, t) {
if (task.id == t.id) { if (task.id == t.id) {
_tasks[i] = task; _tasks[i] = task;
} }
}); });
_buckets.asMap().forEach((i, b) => b.tasks.asMap().forEach((v, t) { _buckets.asMap().forEach((i, b) => b.tasks.asMap().forEach((v, t) {
if (task.id == t.id){ if (task.id == t.id) {
_buckets[i].tasks[v] = task; _buckets[i].tasks[v] = task;
} }
})); }));
notifyListeners(); notifyListeners();
return task; return task;
}); });
} }
Future<void> addBucket({required BuildContext context, required Bucket newBucket, required int listId}) { Future<void> addBucket(
{required BuildContext context,
required Bucket newBucket,
required int listId}) {
notifyListeners(); notifyListeners();
return VikunjaGlobal.of(context).bucketService.add(listId, newBucket) return VikunjaGlobal.of(context)
.bucketService
.add(listId, newBucket)
.then((bucket) { .then((bucket) {
if(bucket == null) if (bucket == null) return null;
return null; _buckets.add(bucket);
_buckets.add(bucket); notifyListeners();
notifyListeners(); });
});
} }
Future<void> updateBucket({required BuildContext context, required Bucket bucket}) { Future<void> updateBucket(
return VikunjaGlobal.of(context).bucketService.update(bucket) {required BuildContext context, required Bucket bucket}) {
return VikunjaGlobal.of(context)
.bucketService
.update(bucket)
.then((rBucket) { .then((rBucket) {
if(rBucket == null) if (rBucket == null) return null;
return null; _buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket; _buckets.sort((a, b) => a.position!.compareTo(b.position!));
_buckets.sort((a, b) => a.position!.compareTo(b.position!)); notifyListeners();
notifyListeners(); });
});
}
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);
notifyListeners();
});
} }
Future<void> moveTaskToBucket({required BuildContext context, required Task? task, int? newBucketId, required int index}) async { Future<void> deleteBucket(
if(task == null) {required BuildContext context,
throw Exception("Task to be moved may not be null"); required int listId,
required int bucketId}) {
return VikunjaGlobal.of(context)
.bucketService
.delete(listId, bucketId)
.then((_) {
_buckets.removeWhere((bucket) => bucket.id == bucketId);
notifyListeners();
});
}
Future<void> moveTaskToBucket(
{required BuildContext context,
required Task? task,
int? newBucketId,
required int index}) async {
if (task == null) throw Exception("Task to be moved may not be null");
final sameBucket = task.bucketId == newBucketId; final sameBucket = task.bucketId == newBucketId;
final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId); final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId);
if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id)) index--; if (sameBucket &&
index >
_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id))
index--;
_buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)].tasks.remove(task); _buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)]
.tasks
.remove(task);
if (index >= _buckets[newBucketIndex].tasks.length) if (index >= _buckets[newBucketIndex].tasks.length)
_buckets[newBucketIndex].tasks.add(task); _buckets[newBucketIndex].tasks.add(task);
else else
_buckets[newBucketIndex].tasks.insert(index, task); _buckets[newBucketIndex].tasks.insert(index, task);
task = await VikunjaGlobal.of(context).taskService.update(task.copyWith( task = await VikunjaGlobal.of(context).taskService.update(task.copyWith(
bucketId: newBucketId, bucketId: newBucketId,
kanbanPosition: calculateItemPosition( kanbanPosition: calculateItemPosition(
positionBefore: index != 0 positionBefore: index != 0
? _buckets[newBucketIndex].tasks[index - 1].kanbanPosition : null, ? _buckets[newBucketIndex].tasks[index - 1].kanbanPosition
positionAfter: index < _buckets[newBucketIndex].tasks.length - 1 : null,
? _buckets[newBucketIndex].tasks[index + 1].kanbanPosition : null, positionAfter: index < _buckets[newBucketIndex].tasks.length - 1
), ? _buckets[newBucketIndex].tasks[index + 1].kanbanPosition
)); : null,
if(task == null) ),
return; ));
if (task == null) return;
_buckets[newBucketIndex].tasks[index] = task; _buckets[newBucketIndex].tasks[index] = task;
// make sure the first 2 tasks don't have 0 kanbanPosition // make sure the first 2 tasks don't have 0 kanbanPosition
Task? secondTask; Task? secondTask;
if (index == 0 && _buckets[newBucketIndex].tasks.length > 1 if (index == 0 &&
&& _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) { _buckets[newBucketIndex].tasks.length > 1 &&
secondTask = await VikunjaGlobal.of(context).taskService.update( _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) {
_buckets[newBucketIndex].tasks[1].copyWith( secondTask = await VikunjaGlobal.of(context)
kanbanPosition: calculateItemPosition( .taskService
positionBefore: task.kanbanPosition, .update(_buckets[newBucketIndex].tasks[1].copyWith(
positionAfter: 1 < _buckets[newBucketIndex].tasks.length - 1 kanbanPosition: calculateItemPosition(
? _buckets[newBucketIndex].tasks[2].kanbanPosition : null, positionBefore: task.kanbanPosition,
), positionAfter: 1 < _buckets[newBucketIndex].tasks.length - 1
)); ? _buckets[newBucketIndex].tasks[2].kanbanPosition
if(secondTask != null) : null,
_buckets[newBucketIndex].tasks[1] = secondTask; ),
));
if (secondTask != null) _buckets[newBucketIndex].tasks[1] = secondTask;
} }
if (_tasks.isNotEmpty) { if (_tasks.isNotEmpty) {
@ -245,8 +276,12 @@ class ListProvider with ChangeNotifier {
_tasks[_tasks.indexWhere((t) => t.id == secondTask!.id)] = secondTask; _tasks[_tasks.indexWhere((t) => t.id == secondTask!.id)] = secondTask;
} }
_buckets[newBucketIndex].tasks[_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id)] = task; _buckets[newBucketIndex].tasks[_buckets[newBucketIndex]
_buckets[newBucketIndex].tasks.sort((a, b) => a.kanbanPosition!.compareTo(b.kanbanPosition!)); .tasks
.indexWhere((t) => t.id == task?.id)] = task;
_buckets[newBucketIndex]
.tasks
.sort((a, b) => a.kanbanPosition!.compareTo(b.kanbanPosition!));
notifyListeners(); notifyListeners();
} }

View File

@ -14,7 +14,6 @@ class ProjectProvider with ChangeNotifier {
List<Task> _tasks = []; List<Task> _tasks = [];
List<Bucket> _buckets = []; List<Bucket> _buckets = [];
bool get taskDragging => _taskDragging; bool get taskDragging => _taskDragging;
set taskDragging(bool value) { set taskDragging(bool value) {
@ -38,7 +37,6 @@ class ProjectProvider with ChangeNotifier {
List<Bucket> get buckets => _buckets; List<Bucket> get buckets => _buckets;
PageStatus _pageStatus = PageStatus.built; PageStatus _pageStatus = PageStatus.built;
PageStatus get pageStatus => _pageStatus; PageStatus get pageStatus => _pageStatus;
@ -49,7 +47,11 @@ class ProjectProvider with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
Future<void> loadTasks({required BuildContext context, required int listId, int page = 1, bool displayDoneTasks = true}) { Future<void> loadTasks(
{required BuildContext context,
required int listId,
int page = 1,
bool displayDoneTasks = true}) {
_tasks = []; _tasks = [];
notifyListeners(); notifyListeners();
@ -59,15 +61,18 @@ class ProjectProvider with ChangeNotifier {
"page": [page.toString()] "page": [page.toString()]
}; };
if(!displayDoneTasks) { if (!displayDoneTasks) {
queryParams.addAll({ queryParams.addAll({
"filter_by": ["done"], "filter_by": ["done"],
"filter_value": ["false"], "filter_value": ["false"],
"sort_by": ["done"], "sort_by": ["done"],
}); });
} }
return VikunjaGlobal.of(context).taskService.getAllByProject(listId, queryParams).then((response) { return VikunjaGlobal.of(context)
if(response == null) { .taskService
.getAllByProject(listId, queryParams)
.then((response) {
if (response == null) {
pageStatus = PageStatus.error; pageStatus = PageStatus.error;
return; return;
} }
@ -79,7 +84,8 @@ class ProjectProvider with ChangeNotifier {
}); });
} }
Future<void> loadBuckets({required BuildContext context, required int listId, int page = 1}) { Future<void> loadBuckets(
{required BuildContext context, required int listId, int page = 1}) {
_buckets = []; _buckets = [];
pageStatus = PageStatus.loading; pageStatus = PageStatus.loading;
notifyListeners(); notifyListeners();
@ -88,8 +94,11 @@ class ProjectProvider with ChangeNotifier {
"page": [page.toString()] "page": [page.toString()]
}; };
return VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) { return VikunjaGlobal.of(context)
if(response == null) { .bucketService
.getAllByList(listId, queryParams)
.then((response) {
if (response == null) {
pageStatus = PageStatus.error; pageStatus = PageStatus.error;
return; return;
} }
@ -103,7 +112,9 @@ class ProjectProvider with ChangeNotifier {
} }
Future<void> addTaskByTitle( Future<void> addTaskByTitle(
{required BuildContext context, required String title, required int projectId}) async{ {required BuildContext context,
required String title,
required int projectId}) async {
final globalState = VikunjaGlobal.of(context); final globalState = VikunjaGlobal.of(context);
if (globalState.currentUser == null) { if (globalState.currentUser == null) {
return; return;
@ -118,13 +129,15 @@ class ProjectProvider with ChangeNotifier {
pageStatus = PageStatus.loading; pageStatus = PageStatus.loading;
return globalState.taskService.add(projectId, newTask).then((task) { return globalState.taskService.add(projectId, newTask).then((task) {
if(task != null) if (task != null) _tasks.insert(0, task);
_tasks.insert(0, task);
pageStatus = PageStatus.success; pageStatus = PageStatus.success;
}); });
} }
Future<void> addTask({required BuildContext context, required Task newTask, required int listId}) { Future<void> addTask(
{required BuildContext context,
required Task newTask,
required int listId}) {
var globalState = VikunjaGlobal.of(context); var globalState = VikunjaGlobal.of(context);
if (newTask.bucketId == null) pageStatus = PageStatus.loading; if (newTask.bucketId == null) pageStatus = PageStatus.loading;
notifyListeners(); notifyListeners();
@ -134,107 +147,129 @@ class ProjectProvider with ChangeNotifier {
pageStatus = PageStatus.error; pageStatus = PageStatus.error;
return; return;
} }
if (_tasks.isNotEmpty) if (_tasks.isNotEmpty) _tasks.insert(0, task);
_tasks.insert(0, task);
if (_buckets.isNotEmpty) { if (_buckets.isNotEmpty) {
final bucket = _buckets[_buckets.indexWhere((b) => task.bucketId == b.id)]; final bucket =
_buckets[_buckets.indexWhere((b) => task.bucketId == b.id)];
bucket.tasks.add(task); bucket.tasks.add(task);
} }
pageStatus = PageStatus.success; pageStatus = PageStatus.success;
}); });
} }
Future<Task?> updateTask({required BuildContext context, required Task task}) { Future<Task?> updateTask(
{required BuildContext context, required Task task}) {
return VikunjaGlobal.of(context).taskService.update(task).then((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. // 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 definitely fix it later. // This is enough for now (it works) but we should definitely fix it later.
if(task == null) if (task == null) return null;
return null;
_tasks.asMap().forEach((i, t) { _tasks.asMap().forEach((i, t) {
if (task.id == t.id) { if (task.id == t.id) {
_tasks[i] = task; _tasks[i] = task;
} }
}); });
_buckets.asMap().forEach((i, b) => b.tasks.asMap().forEach((v, t) { _buckets.asMap().forEach((i, b) => b.tasks.asMap().forEach((v, t) {
if (task.id == t.id){ if (task.id == t.id) {
_buckets[i].tasks[v] = task; _buckets[i].tasks[v] = task;
} }
})); }));
notifyListeners(); notifyListeners();
return task; return task;
}); });
} }
Future<void> addBucket({required BuildContext context, required Bucket newBucket, required int listId}) { Future<void> addBucket(
{required BuildContext context,
required Bucket newBucket,
required int listId}) {
notifyListeners(); notifyListeners();
return VikunjaGlobal.of(context).bucketService.add(listId, newBucket) return VikunjaGlobal.of(context)
.bucketService
.add(listId, newBucket)
.then((bucket) { .then((bucket) {
if(bucket == null) if (bucket == null) return null;
return null;
_buckets.add(bucket); _buckets.add(bucket);
notifyListeners(); notifyListeners();
}); });
} }
Future<void> updateBucket({required BuildContext context, required Bucket bucket}) { Future<void> updateBucket(
return VikunjaGlobal.of(context).bucketService.update(bucket) {required BuildContext context, required Bucket bucket}) {
return VikunjaGlobal.of(context)
.bucketService
.update(bucket)
.then((rBucket) { .then((rBucket) {
if(rBucket == null) if (rBucket == null) return null;
return null;
_buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket; _buckets[_buckets.indexWhere((b) => rBucket.id == b.id)] = rBucket;
_buckets.sort((a, b) => a.position!.compareTo(b.position!)); _buckets.sort((a, b) => a.position!.compareTo(b.position!));
notifyListeners(); notifyListeners();
}); });
} }
Future<void> deleteBucket({required BuildContext context, required int listId, required int bucketId}) { Future<void> deleteBucket(
return VikunjaGlobal.of(context).bucketService.delete(listId, bucketId) {required BuildContext context,
required int listId,
required int bucketId}) {
return VikunjaGlobal.of(context)
.bucketService
.delete(listId, bucketId)
.then((_) { .then((_) {
_buckets.removeWhere((bucket) => bucket.id == bucketId); _buckets.removeWhere((bucket) => bucket.id == bucketId);
notifyListeners(); notifyListeners();
}); });
} }
Future<void> moveTaskToBucket({required BuildContext context, required Task? task, int? newBucketId, required int index}) async { Future<void> moveTaskToBucket(
if(task == null) {required BuildContext context,
throw Exception("Task to be moved may not be null"); required Task? task,
int? newBucketId,
required int index}) async {
if (task == null) throw Exception("Task to be moved may not be null");
final sameBucket = task.bucketId == newBucketId; final sameBucket = task.bucketId == newBucketId;
final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId); final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId);
if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id)) index--; if (sameBucket &&
index >
_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id))
index--;
_buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)].tasks.remove(task); _buckets[_buckets.indexWhere((b) => b.id == task?.bucketId)]
.tasks
.remove(task);
if (index >= _buckets[newBucketIndex].tasks.length) if (index >= _buckets[newBucketIndex].tasks.length)
_buckets[newBucketIndex].tasks.add(task); _buckets[newBucketIndex].tasks.add(task);
else else
_buckets[newBucketIndex].tasks.insert(index, task); _buckets[newBucketIndex].tasks.insert(index, task);
task = await VikunjaGlobal.of(context).taskService.update(task.copyWith( task = await VikunjaGlobal.of(context).taskService.update(task.copyWith(
bucketId: newBucketId, bucketId: newBucketId,
kanbanPosition: calculateItemPosition( kanbanPosition: calculateItemPosition(
positionBefore: index != 0 positionBefore: index != 0
? _buckets[newBucketIndex].tasks[index - 1].kanbanPosition : null, ? _buckets[newBucketIndex].tasks[index - 1].kanbanPosition
positionAfter: index < _buckets[newBucketIndex].tasks.length - 1 : null,
? _buckets[newBucketIndex].tasks[index + 1].kanbanPosition : null, positionAfter: index < _buckets[newBucketIndex].tasks.length - 1
), ? _buckets[newBucketIndex].tasks[index + 1].kanbanPosition
)); : null,
if(task == null) ),
return; ));
if (task == null) return;
_buckets[newBucketIndex].tasks[index] = task; _buckets[newBucketIndex].tasks[index] = task;
// make sure the first 2 tasks don't have 0 kanbanPosition // make sure the first 2 tasks don't have 0 kanbanPosition
Task? secondTask; Task? secondTask;
if (index == 0 && _buckets[newBucketIndex].tasks.length > 1 if (index == 0 &&
&& _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) { _buckets[newBucketIndex].tasks.length > 1 &&
secondTask = await VikunjaGlobal.of(context).taskService.update( _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) {
_buckets[newBucketIndex].tasks[1].copyWith( secondTask = await VikunjaGlobal.of(context)
kanbanPosition: calculateItemPosition( .taskService
positionBefore: task.kanbanPosition, .update(_buckets[newBucketIndex].tasks[1].copyWith(
positionAfter: 1 < _buckets[newBucketIndex].tasks.length - 1 kanbanPosition: calculateItemPosition(
? _buckets[newBucketIndex].tasks[2].kanbanPosition : null, positionBefore: task.kanbanPosition,
), positionAfter: 1 < _buckets[newBucketIndex].tasks.length - 1
)); ? _buckets[newBucketIndex].tasks[2].kanbanPosition
if(secondTask != null) : null,
_buckets[newBucketIndex].tasks[1] = secondTask; ),
));
if (secondTask != null) _buckets[newBucketIndex].tasks[1] = secondTask;
} }
if (_tasks.isNotEmpty) { if (_tasks.isNotEmpty) {
@ -243,8 +278,12 @@ class ProjectProvider with ChangeNotifier {
_tasks[_tasks.indexWhere((t) => t.id == secondTask!.id)] = secondTask; _tasks[_tasks.indexWhere((t) => t.id == secondTask!.id)] = secondTask;
} }
_buckets[newBucketIndex].tasks[_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task?.id)] = task; _buckets[newBucketIndex].tasks[_buckets[newBucketIndex]
_buckets[newBucketIndex].tasks.sort((a, b) => a.kanbanPosition!.compareTo(b.kanbanPosition!)); .tasks
.indexWhere((t) => t.id == task?.id)] = task;
_buckets[newBucketIndex]
.tasks
.sort((a, b) => a.kanbanPosition!.compareTo(b.kanbanPosition!));
notifyListeners(); notifyListeners();
} }

View File

@ -17,11 +17,13 @@ class FancyButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ElevatedButton(onPressed: onPressed, return ElevatedButton(
onPressed: onPressed,
child: SizedBox( child: SizedBox(
width: width, width: width,
child: Center(child: child), child: Center(child: child),
),); ),
);
return Padding( return Padding(
padding: vStandardVerticalPadding, padding: vStandardVerticalPadding,
child: Container( child: Container(

View File

@ -14,7 +14,9 @@ class VikunjaButtonText extends StatelessWidget {
return Text(text); return Text(text);
return Text( return Text(
text, text,
style: TextStyle(color: Theme.of(context).primaryTextTheme.labelMedium?.color, fontWeight: FontWeight.w600), style: TextStyle(
color: Theme.of(context).primaryTextTheme.labelMedium?.color,
fontWeight: FontWeight.w600),
); );
} }
} }

View File

@ -31,4 +31,4 @@ const vStandardHorizontalPadding = EdgeInsets.symmetric(horizontal: 5.0);
const vStandardPadding = EdgeInsets.symmetric(horizontal: 5.0, vertical: 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");
var vDateFormatShort = DateFormat("d MMM yyyy, H:mm"); var vDateFormatShort = DateFormat("d MMM yyyy, H:mm");

View File

@ -4,13 +4,15 @@ import 'package:flutter/material.dart';
import 'package:vikunja_app/theme/constants.dart'; import 'package:vikunja_app/theme/constants.dart';
ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light()); ThemeData buildVikunjaTheme() => _buildVikunjaTheme(ThemeData.light());
ThemeData buildVikunjaDarkTheme() => _buildVikunjaTheme(ThemeData.dark(), isDark: true); ThemeData buildVikunjaDarkTheme() =>
_buildVikunjaTheme(ThemeData.dark(), isDark: true);
ThemeData buildVikunjaMaterialLightTheme() { ThemeData buildVikunjaMaterialLightTheme() {
return ThemeData.light().copyWith( return ThemeData.light().copyWith(
useMaterial3: true, useMaterial3: true,
); );
} }
ThemeData buildVikunjaMaterialDarkTheme() { ThemeData buildVikunjaMaterialDarkTheme() {
return ThemeData.dark().copyWith( return ThemeData.dark().copyWith(
useMaterial3: true, useMaterial3: true,
@ -45,11 +47,8 @@ ThemeData _buildVikunjaTheme(ThemeData base, {bool isDark = false}) {
), ),
inputDecorationTheme: InputDecorationTheme( inputDecorationTheme: InputDecorationTheme(
enabledBorder: UnderlineInputBorder( enabledBorder: UnderlineInputBorder(
borderSide: const BorderSide(color: Colors.grey, width: 1) borderSide: const BorderSide(color: Colors.grey, width: 1)),
),
), ),
dividerTheme: DividerThemeData( dividerTheme: DividerThemeData(
color: () { color: () {
return isDark ? Colors.white10 : Colors.black12; return isDark ? Colors.white10 : Colors.black12;
@ -60,10 +59,11 @@ ThemeData _buildVikunjaTheme(ThemeData base, {bool isDark = false}) {
// Make bottomNavigationBar backgroundColor darker to provide more separation // Make bottomNavigationBar backgroundColor darker to provide more separation
backgroundColor: () { backgroundColor: () {
final _hslColor = HSLColor.fromColor( final _hslColor = HSLColor.fromColor(
base.bottomNavigationBarTheme.backgroundColor base.bottomNavigationBarTheme.backgroundColor ??
?? base.scaffoldBackgroundColor base.scaffoldBackgroundColor);
); return _hslColor
return _hslColor.withLightness(max(_hslColor.lightness - 0.03, 0)).toColor(); .withLightness(max(_hslColor.lightness - 0.03, 0))
.toColor();
}(), }(),
), ),
); );

View File

@ -18,4 +18,4 @@ double calculateItemPosition({double? positionBefore, double? positionAfter}) {
// in the middle (positionBefore != null && positionAfter != null) // in the middle (positionBefore != null && positionAfter != null)
return (positionBefore! + positionAfter!) / 2; return (positionBefore! + positionAfter!) / 2;
} }

View File

@ -46,4 +46,3 @@ CheckboxStatistics getCheckboxStatistics(String text) {
checked: checkboxes.checked.length, checked: checkboxes.checked.length,
); );
} }

View File

@ -1,21 +1,20 @@
String durationToHumanReadable(Duration dur) { String durationToHumanReadable(Duration dur) {
var durString = ''; var durString = '';
if(dur.inDays.abs() > 1) if (dur.inDays.abs() > 1)
durString = dur.inDays.abs().toString() + " days"; durString = dur.inDays.abs().toString() + " days";
else if(dur.inDays.abs() == 1) else if (dur.inDays.abs() == 1)
durString = dur.inDays.abs().toString() + " day"; durString = dur.inDays.abs().toString() + " day";
else if (dur.inHours.abs() > 1)
else if(dur.inHours.abs() > 1)
durString = dur.inHours.abs().toString() + " hours"; durString = dur.inHours.abs().toString() + " hours";
else if(dur.inHours.abs() == 1) else if (dur.inHours.abs() == 1)
durString = dur.inHours.abs().toString() + " hour"; durString = dur.inHours.abs().toString() + " hour";
else if (dur.inMinutes.abs() > 1)
else if(dur.inMinutes.abs() > 1)
durString = dur.inMinutes.abs().toString() + " minutes"; durString = dur.inMinutes.abs().toString() + " minutes";
else if(dur.inMinutes.abs() == 1) else if (dur.inMinutes.abs() == 1)
durString = dur.inMinutes.abs().toString() + " minute"; durString = dur.inMinutes.abs().toString() + " minute";
else durString = "less than a minute"; else
durString = "less than a minute";
if (dur.isNegative) return durString + " ago"; if (dur.isNegative) return durString + " ago";
return "in " + durString; return "in " + durString;
} }

View File

@ -31,8 +31,7 @@ priorityFromString(String? priority) {
case 'DO NOW': case 'DO NOW':
return 5; return 5;
default: default:
// unset // unset
return 0; return 0;
} }
} }

View File

@ -66,6 +66,6 @@ Duration? getDurationFromType(String? value, String? type) {
case 'Years': case 'Years':
return Duration(days: val * 365); return Duration(days: val * 365);
} }
return null; return null;
} }

View File

@ -7,7 +7,8 @@ import 'package:vikunja_app/models/user.dart';
void main() { void main() {
test('label color from json', () { test('label color from json', () {
final String json = '{"TaskID": 123,"id": 1,"title": "this","description": "","hex_color": "e8e8e8","created_by":{"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325},"created": 1552903790,"updated": 1552903790}'; final String json =
'{"TaskID": 123,"id": 1,"title": "this","description": "","hex_color": "e8e8e8","created_by":{"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325},"created": 1552903790,"updated": 1552903790}';
final JsonDecoder _decoder = new JsonDecoder(); final JsonDecoder _decoder = new JsonDecoder();
Label label = Label.fromJson(_decoder.convert(json)); Label label = Label.fromJson(_decoder.convert(json));
@ -15,9 +16,14 @@ void main() {
}); });
test('hex color string from object', () { test('hex color string from object', () {
Label label = Label(id: 1, title: '', color: Color(0xFFe8e8e8), createdBy: User(id: 0, username: '')); Label label = Label(
id: 1,
title: '',
color: Color(0xFFe8e8e8),
createdBy: User(id: 0, username: ''));
var json = label.toJSON(); var json = label.toJSON();
expect(json.toString(), '{id: 1, title: , description: null, hex_color: e8e8e8, created_by: {id: 0, username: ,}, updated: null, created: null}'); expect(json.toString(),
'{id: 1, title: , description: null, hex_color: e8e8e8, created_by: {id: 0, username: ,}, updated: null, created: null}');
}); });
} }

View File

@ -5,7 +5,8 @@ import 'package:test/test.dart';
void main() { void main() {
test('Check encoding with all values set', () { test('Check encoding with all values set', () {
final String json = '{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": [1543834800,1544612400],"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}'; final String json =
'{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": [1543834800,1544612400],"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
final JsonDecoder _decoder = new JsonDecoder(); final JsonDecoder _decoder = new JsonDecoder();
final task = Task.fromJson(_decoder.convert(json)); final task = Task.fromJson(_decoder.convert(json));
@ -17,19 +18,25 @@ void main() {
DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000), DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000),
DateTime.fromMillisecondsSinceEpoch(1544612400 * 1000), DateTime.fromMillisecondsSinceEpoch(1544612400 * 1000),
]); ]);
expect(task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000)); expect(
task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(task.repeatAfter, Duration(seconds: 3600)); expect(task.repeatAfter, Duration(seconds: 3600));
expect(task.parentTaskId, 0); expect(task.parentTaskId, 0);
expect(task.priority, 100); expect(task.priority, 100);
expect(task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000)); expect(
expect(task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000)); task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(
task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
expect(task.labels, null); expect(task.labels, null);
expect(task.subtasks, null); expect(task.subtasks, null);
expect(task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000)); expect(
expect(task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000)); task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
expect(
task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
}); });
test('Check encoding with reminder dates as null', () { test('Check encoding with reminder dates as null', () {
final String json = '{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": null,"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}'; final String json =
'{"id": 1,"text": "test","description": "Lorem Ipsum","done": true,"dueDate": 1543834800,"reminderDates": null,"repeatAfter": 3600,"parentTaskID": 0,"priority": 100,"startDate": 1543834800,"endDate": 1543835000,"assignees": null,"labels": null,"subtasks": null,"created": 1542465818,"updated": 1552771527,"createdBy": {"id": 1,"username": "user","email": "test@example.com","created": 1537855131,"updated": 1545233325}}';
final JsonDecoder _decoder = new JsonDecoder(); final JsonDecoder _decoder = new JsonDecoder();
final task = Task.fromJson(_decoder.convert(json)); final task = Task.fromJson(_decoder.convert(json));
@ -38,15 +45,20 @@ void main() {
expect(task.description, 'Lorem Ipsum'); expect(task.description, 'Lorem Ipsum');
expect(task.done, true); expect(task.done, true);
expect(task.reminderDates, null); expect(task.reminderDates, null);
expect(task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000)); expect(
task.dueDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(task.repeatAfter, Duration(seconds: 3600)); expect(task.repeatAfter, Duration(seconds: 3600));
expect(task.parentTaskId, 0); expect(task.parentTaskId, 0);
expect(task.priority, 100); expect(task.priority, 100);
expect(task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000)); expect(
expect(task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000)); task.startDate, DateTime.fromMillisecondsSinceEpoch(1543834800 * 1000));
expect(
task.endDate, DateTime.fromMillisecondsSinceEpoch(1543835000 * 1000));
expect(task.labels, null); expect(task.labels, null);
expect(task.subtasks, null); expect(task.subtasks, null);
expect(task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000)); expect(
expect(task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000)); task.created, DateTime.fromMillisecondsSinceEpoch(1542465818 * 1000));
expect(
task.updated, DateTime.fromMillisecondsSinceEpoch(1552771527 * 1000));
}); });
} }