Add API implementations of List, Namespace, Task, User

Use Services in order to retrieve data
This commit is contained in:
Jonas Franz 2018-09-17 18:16:50 +02:00
parent 1994892b63
commit f7db5324aa
No known key found for this signature in database
GPG Key ID: C8287A01D593AC1D
16 changed files with 454 additions and 86 deletions

View File

@ -31,6 +31,12 @@ class Client {
.then(_handleResponse);
}
Future<dynamic> delete(String url) {
return http
.delete('${this.base}$url', headers: _headers)
.then(_handleResponse);
}
Future<dynamic> post(String url, {dynamic body}) {
return http
.post('${this.base}$url',
@ -38,6 +44,13 @@ class Client {
.then(_handleResponse);
}
Future<dynamic> put(String url, {dynamic body}) {
return http
.put('${this.base}$url',
headers: _headers, body: _encoder.convert(body))
.then(_handleResponse);
}
dynamic _handleResponse(http.Response response) {
if (response.statusCode < 200 ||
response.statusCode > 400 ||

View File

@ -0,0 +1,46 @@
import 'dart:async';
import 'package:fluttering_vikunja/api/client.dart';
import 'package:fluttering_vikunja/api/service.dart';
import 'package:fluttering_vikunja/models/task.dart';
import 'package:fluttering_vikunja/service/services.dart';
class ListAPIService extends APIService implements ListService {
ListAPIService(Client client) : super(client);
@override
Future<TaskList> create(namespaceId, TaskList tl) {
return client
.put('/namespaces/$namespaceId/lists', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
}
@override
Future delete(int listId) {
return client.delete('/lists/$listId').then((_) {});
}
@override
Future<TaskList> get(int listId) {
return client.get('/lists/$listId').then((map) => TaskList.fromJson(map));
}
@override
Future<List<TaskList>> getAll() {
return client.get('/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
}
@override
Future<List<TaskList>> getByNamespace(int namespaceId) {
return client.get('/namespaces/$namespaceId/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
}
@override
Future<TaskList> update(TaskList tl) {
return client
.put('/lists/${tl.id}', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
}
}

View File

@ -0,0 +1,42 @@
import 'dart:async';
import 'package:fluttering_vikunja/api/client.dart';
import 'package:fluttering_vikunja/api/service.dart';
import 'package:fluttering_vikunja/models/namespace.dart';
import 'package:fluttering_vikunja/service/services.dart';
class NamespaceAPIService extends APIService implements NamespaceService {
NamespaceAPIService(Client client) : super(client);
@override
Future<Namespace> create(Namespace ns) {
return client
.put('/namespaces', body: ns.toJSON())
.then((map) => Namespace.fromJson(map));
}
@override
Future delete(int namespaceId) {
return client.delete('/namespaces/$namespaceId');
}
@override
Future<Namespace> get(int namespaceId) {
return client
.get('/namespaces/$namespaceId')
.then((map) => Namespace.fromJson(map));
}
@override
Future<List<Namespace>> getAll() {
return client.get('/namespaces').then(
(list) => convertList(list, (result) => Namespace.fromJson(result)));
}
@override
Future<Namespace> update(Namespace ns) {
return client
.post('/namespaces/${ns.id}', body: ns.toJSON())
.then((map) => Namespace.fromJson(map));
}
}

19
lib/api/service.dart Normal file
View File

@ -0,0 +1,19 @@
import 'package:fluttering_vikunja/api/client.dart';
import 'package:meta/meta.dart';
class APIService {
final Client _client;
@protected
Client get client => _client;
APIService(this._client);
@protected
List<T> convertList<T>(dynamic value, Mapper<T> mapper) {
if (value == null) return [];
return (value as List<dynamic>).map((map) => mapper(map)).toList();
}
}
typedef T Mapper<T>(Map<String, dynamic> json);

View File

@ -0,0 +1,29 @@
import 'dart:async';
import 'package:fluttering_vikunja/api/client.dart';
import 'package:fluttering_vikunja/api/service.dart';
import 'package:fluttering_vikunja/models/task.dart';
import 'package:fluttering_vikunja/service/services.dart';
class TaskAPIService extends APIService implements TaskService {
TaskAPIService(Client client) : super(client);
@override
Future<Task> add(int listId, Task task) {
return client
.put('/lists/$listId', body: task.toJSON())
.then((map) => Task.fromJson(map));
}
@override
Future delete(int taskId) {
return client.delete('/tasks/$taskId');
}
@override
Future<Task> update(Task task) {
return client
.post('/tasks/${task.id}', body: task.toJSON())
.then((map) => Task.fromJson(map));
}
}

View File

@ -1,27 +1,26 @@
import 'dart:async';
import 'package:fluttering_vikunja/api/client.dart';
import 'package:fluttering_vikunja/api/service.dart';
import 'package:fluttering_vikunja/models/user.dart';
import 'package:fluttering_vikunja/service/services.dart';
class UserAPIService implements UserService {
final Client _client;
UserAPIService(this._client);
class UserAPIService extends APIService implements UserService {
UserAPIService(Client client) : super(client);
@override
Future<UserTokenPair> login(String username, password) async {
var token = await _client.post('/login', body: {
var token = await client.post('/login', body: {
'username': username,
'password': password
}).then((map) => map['token']);
return UserAPIService(Client(token, _client.base))
return UserAPIService(Client(token, client.base))
.getCurrentUser()
.then((user) => UserTokenPair(user, token));
}
@override
Future<User> getCurrentUser() {
return _client.get('/user').then((map) => User.fromJson(map));
return client.get('/user').then((map) => User.fromJson(map));
}
}

View File

@ -1,50 +1,121 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:fluttering_vikunja/global.dart';
import 'package:fluttering_vikunja/models/namespace.dart';
import 'package:fluttering_vikunja/models/task.dart';
import 'package:fluttering_vikunja/pages/list_page.dart';
class NamespaceFragment extends StatefulWidget {
final String namespace;
NamespaceFragment({this.namespace}) : super(key: Key(namespace));
final Namespace namespace;
NamespaceFragment({this.namespace})
: super(key: Key(namespace.id.toString()));
@override
_NamespaceFragmentState createState() => new _NamespaceFragmentState();
}
class _NamespaceFragmentState extends State<NamespaceFragment> {
Set<String> _lists = Set.from(
["Cupertino List", "Material List", "Shopping List", "NAS List"]);
List<TaskList> _lists = [];
@override
Widget build(BuildContext context) {
return new ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context,
tiles: _lists.map((name) => Dismissible(
key: Key(name),
direction: DismissDirection.startToEnd,
child: ListTile(
title: new Text(name),
onTap: () => _openList(context, name),
trailing: Icon(Icons.arrow_right),
),
background: Container(
color: Colors.red,
child: const ListTile(
leading:
Icon(Icons.delete, color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
setState(() => _lists.remove(name));
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text("$name removed")));
},
))).toList(),
return Scaffold(
body: new ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context,
tiles: _lists.map((ls) => Dismissible(
key: Key(ls.id.toString()),
direction: DismissDirection.startToEnd,
child: ListTile(
title: new Text(ls.title),
onTap: () => _openList(context, ls),
trailing: Icon(Icons.arrow_right),
),
background: Container(
color: Colors.red,
child: const ListTile(
leading: Icon(Icons.delete,
color: Colors.white, size: 36.0)),
),
onDismissed: (direction) {
_removeList(ls).then((_) => Scaffold.of(context)
.showSnackBar(
SnackBar(content: Text("${ls.title} removed"))));
},
))).toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addListDialog(), child: const Icon(Icons.add)),
);
}
_openList(BuildContext context, String name) {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateLists();
}
Future _removeList(TaskList list) {
return VikunjaGlobal.of(context)
.listService
.delete(list.id)
.then((_) => _updateLists());
}
_updateLists() {
VikunjaGlobal.of(context)
.listService
.getByNamespace(widget.namespace.id)
.then((lists) => setState(() => this._lists = lists));
}
_openList(BuildContext context, TaskList list) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ListPage(listName: name)));
MaterialPageRoute(builder: (context) => ListPage(taskList: list)));
}
_addListDialog() {
var textController = new TextEditingController();
showDialog(
context: context,
child: new AlertDialog(
contentPadding: const EdgeInsets.all(16.0),
content: new Row(children: <Widget>[
Expanded(
child: new TextField(
autofocus: true,
decoration: new InputDecoration(
labelText: 'List Name', hintText: 'eg. Shopping List'),
controller: textController,
),
)
]),
actions: <Widget>[
new FlatButton(
child: const Text('CANCEL'),
onPressed: () => Navigator.pop(context),
),
new FlatButton(
child: const Text('ADD'),
onPressed: () {
if (textController.text.isNotEmpty) {
_addList(textController.text);
}
Navigator.pop(context);
},
)
],
),
);
}
_addList(String name) {
VikunjaGlobal.of(context)
.listService
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: []))
.then((_) => setState(() {}));
}
}

View File

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:fluttering_vikunja/api/client.dart';
import 'package:fluttering_vikunja/api/list_implementation.dart';
import 'package:fluttering_vikunja/api/namespace_implementation.dart';
import 'package:fluttering_vikunja/api/task_implementation.dart';
import 'package:fluttering_vikunja/api/user_implementation.dart';
import 'package:fluttering_vikunja/managers/user.dart';
import 'package:fluttering_vikunja/models/user.dart';
@ -36,6 +39,9 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
UserManager get userManager => new UserManager(_storage);
UserService get userService => new UserAPIService(_client);
UserService newLoginService(base) => new UserAPIService(Client(null, base));
NamespaceService get namespaceService => new NamespaceAPIService(client);
TaskService get taskService => new TaskAPIService(client);
ListService get listService => new ListAPIService(client);
@override
void initState() {
@ -84,12 +90,27 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
});
return;
}
var client = Client(token, base);
var loadedCurrentUser;
try {
loadedCurrentUser = await UserAPIService(client).getCurrentUser();
} on ApiException catch (e) {
if (e.errorCode ~/ 100 == 4) {
setState(() {
_client = null;
_currentUser = null;
_loading = false;
});
return;
}
loadedCurrentUser = User(int.tryParse(currentUser), "", "");
} catch (otherExceptions) {
loadedCurrentUser = User(int.tryParse(currentUser), "", "");
}
setState(() {
_client = Client(token, base);
});
var loadedCurrentUser = await userService.getCurrentUser();
setState(() {
_client = client;
_currentUser = loadedCurrentUser;
_loading = false;
});
}

View File

@ -6,16 +6,21 @@ import 'package:fluttering_vikunja/pages/home_page.dart';
import 'package:fluttering_vikunja/pages/login_page.dart';
import 'package:fluttering_vikunja/style.dart';
void main() => runApp(new VikunjaApp());
void main() => runApp(VikunjaGlobal(
child: new VikunjaApp(home: HomePage()),
login: new VikunjaApp(home: LoginPage())));
class VikunjaApp extends StatelessWidget {
final Widget home;
const VikunjaApp({Key key, this.home}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Vikunja',
theme: buildVikunjaTheme(),
home: VikunjaGlobal(child: new HomePage(), login: new LoginPage()),
home: this.home,
);
}
}

View File

@ -22,4 +22,12 @@ class Namespace {
created = DateTime.fromMillisecondsSinceEpoch(json['created']),
updated = DateTime.fromMillisecondsSinceEpoch(json['updated']),
owner = User.fromJson(json['owner']);
toJSON() => {
"created": created?.millisecondsSinceEpoch,
"updated": updated?.millisecondsSinceEpoch,
"name": name,
"owner": owner?.toJSON(),
"description": description
};
}

View File

@ -16,7 +16,7 @@ class Task {
this.due,
@required this.text,
this.description,
this.done,
@required this.done,
@required this.owner});
Task.fromJson(Map<String, dynamic> json)
@ -29,6 +29,18 @@ class Task {
text = json['text'],
done = json['done'],
owner = User.fromJson(json['createdBy']);
toJSON() => {
'id': id,
'updated': updated?.millisecondsSinceEpoch,
'created': created?.millisecondsSinceEpoch,
'reminderDate': reminder?.millisecondsSinceEpoch,
'dueDate': due?.millisecondsSinceEpoch,
'description': description,
'text': text,
'done': done ?? false,
'createdBy': owner?.toJSON()
};
}
class TaskList {
@ -54,5 +66,17 @@ class TaskList {
title = json['title'],
updated = DateTime.fromMillisecondsSinceEpoch(json['updated']),
created = DateTime.fromMillisecondsSinceEpoch(json['created']),
tasks = json['tasks'].map((taskJson) => Task.fromJson(taskJson));
tasks = (json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();
toJSON() {
return {
"created": this.created?.millisecondsSinceEpoch,
"updated": this.updated?.millisecondsSinceEpoch,
"id": this.id,
"title": this.title,
"owner": this.owner?.toJSON()
};
}
}

View File

@ -9,6 +9,8 @@ class User {
: id = json['id'],
email = json['email'],
username = json['username'];
toJSON() => {"id": this.id, "email": this.email, "username": this.username};
}
class UserTokenPair {

View File

@ -3,6 +3,8 @@ import 'package:fluttering_vikunja/components/GravatarImage.dart';
import 'package:fluttering_vikunja/fragments/namespace.dart';
import 'package:fluttering_vikunja/fragments/placeholder.dart';
import 'package:fluttering_vikunja/global.dart';
import 'package:fluttering_vikunja/models/namespace.dart';
import 'package:fluttering_vikunja/models/task.dart';
import 'package:fluttering_vikunja/models/user.dart';
class HomePage extends StatefulWidget {
@ -11,14 +13,18 @@ class HomePage extends StatefulWidget {
}
class HomePageState extends State<HomePage> {
List<String> namespaces = ["Jonas's namespace", 'Another namespace'];
List<Namespace> _namespaces = [];
Namespace get _currentNamespace =>
_selectedDrawerIndex >= 0 && _selectedDrawerIndex < _namespaces.length
? _namespaces[_selectedDrawerIndex]
: null;
int _selectedDrawerIndex = -1;
_getDrawerItemWidget(int pos) {
if (pos == -1) {
return new PlaceholderFragment();
}
return new NamespaceFragment(namespace: namespaces[pos]);
return new NamespaceFragment(namespace: _namespaces[pos]);
}
_onSelectItem(int index) {
@ -26,7 +32,7 @@ class HomePageState extends State<HomePage> {
Navigator.of(context).pop();
}
_addNamespace() {
_addNamespaceDialog() {
var textController = new TextEditingController();
showDialog(
context: context,
@ -50,8 +56,9 @@ class HomePageState extends State<HomePage> {
new FlatButton(
child: const Text('ADD'),
onPressed: () {
if (textController.text.isNotEmpty)
setState(() => namespaces.add(textController.text));
if (textController.text.isNotEmpty) {
_addNamespace(textController.text);
}
Navigator.pop(context);
},
)
@ -60,28 +67,42 @@ class HomePageState extends State<HomePage> {
);
}
_addNamespace(String name) {
VikunjaGlobal.of(context)
.namespaceService
.create(Namespace(id: null, name: name))
.then((_) => _updateNamespaces());
}
_updateNamespaces() {
VikunjaGlobal.of(context).namespaceService.getAll().then((result) {
setState(() {
_namespaces = result;
});
});
}
@override
void initState() {
super.initState();
void didChangeDependencies() {
super.didChangeDependencies();
_updateNamespaces();
}
@override
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
List<Widget> drawerOptions = <Widget>[];
namespaces.asMap().forEach((i, namespace) => drawerOptions.add(new ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
_namespaces
.asMap()
.forEach((i, namespace) => drawerOptions.add(new ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace.name),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
return new Scaffold(
appBar: AppBar(
title: new Text(_selectedDrawerIndex == -1
? 'Vakunja'
: namespaces[_selectedDrawerIndex]),
),
appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vakunja')),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
@ -110,7 +131,7 @@ class HomePageState extends State<HomePage> {
child: new ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespace(),
onTap: () => _addNamespaceDialog(),
),
),
])),

View File

@ -1,46 +1,82 @@
import 'package:flutter/material.dart';
import 'package:fluttering_vikunja/global.dart';
import 'package:fluttering_vikunja/models/task.dart';
class ListPage extends StatefulWidget {
final String listName;
final TaskList taskList;
ListPage({this.listName}) : super(key: Key(listName));
ListPage({this.taskList}) : super(key: Key(taskList.id.toString()));
@override
_ListPageState createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
Map<String, bool> items = {"Butter": true, "Milch": false};
TaskList items;
@override
void initState() {
items = TaskList(
id: widget.taskList.id, title: widget.taskList.title, tasks: []);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(widget.listName),
title: new Text(items.title),
),
body: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context,
tiles: items
.map((item, checked) => MapEntry(
item,
CheckboxListTile(
title: Text(item),
tiles: items?.tasks?.map((task) => CheckboxListTile(
title: Text(task.text),
controlAffinity: ListTileControlAffinity.leading,
value: checked,
onChanged: (bool value) =>
setState(() => items[item] = value),
)))
.values)
value: task.done ?? false,
subtitle: task.description == null
? null
: Text(task.description),
onChanged: (bool value) => _updateTask(task, value),
)) ??
[])
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addItem(), child: Icon(Icons.add)),
onPressed: () => _addItemDialog(), child: Icon(Icons.add)),
);
}
_addItem() {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateList();
}
_updateTask(Task task, bool checked) {
// TODO use copyFrom
VikunjaGlobal.of(context)
.taskService
.update(Task(
id: task.id,
done: checked,
text: task.text,
description: task.description,
owner: null,
))
.then((_) => _updateList());
}
_updateList() {
VikunjaGlobal.of(context).listService.get(widget.taskList.id).then((tasks) {
setState(() {
items = tasks;
});
});
}
_addItemDialog() {
var textController = new TextEditingController();
showDialog(
context: context,
@ -64,8 +100,7 @@ class _ListPageState extends State<ListPage> {
new FlatButton(
child: const Text('ADD'),
onPressed: () {
if (textController.text.isNotEmpty)
setState(() => items[textController.text] = false);
if (textController.text.isNotEmpty) _addItem(textController.text);
Navigator.pop(context);
},
)
@ -73,4 +108,21 @@ class _ListPageState extends State<ListPage> {
),
);
}
_addItem(String name) {
var globalState = VikunjaGlobal.of(context);
globalState.taskService
.add(
items.id,
Task(
id: null,
text: name,
owner: globalState.currentUser,
done: false))
.then((task) {
setState(() {
items.tasks.add(task);
});
}).then((_) => _updateList());
}
}

View File

@ -27,7 +27,7 @@ var _lists = {
1: TaskList(
id: 1,
title: 'List 1',
tasks: _tasks.values,
tasks: _tasks.values.toList(),
owner: _users[1],
description: 'A nice list',
created: DateTime.now(),
@ -81,7 +81,8 @@ class MockedNamespaceService implements NamespaceService {
class MockedListService implements ListService {
@override
Future<TaskList> create(TaskList tl) {
Future<TaskList> create(namespaceId, TaskList tl) {
_nsLists[namespaceId].add(tl.id);
return Future.value(_lists[tl.id] = tl);
}
@ -111,7 +112,7 @@ class MockedListService implements ListService {
Future<TaskList> update(TaskList tl) {
if (!_lists.containsKey(tl))
throw Exception('TaskList ${tl.id} does not exists');
return create(tl);
return Future.value(_lists[tl.id] = tl);
}
}
@ -126,8 +127,22 @@ class MockedTaskService implements TaskService {
@override
Future<Task> update(Task task) {
_lists.forEach((_, list) {
if (list.tasks.where((t) => t.id == task.id).length > 0) {
list.tasks.removeWhere((t) => t.id == task.id);
list.tasks.add(task);
}
});
return Future.value(_tasks[task.id] = task);
}
@override
Future<Task> add(int listId, Task task) {
var id = _tasks.keys.last + 1;
_tasks[id] = task;
_lists[listId].tasks.add(task);
return Future.value(task);
}
}
class MockedUserService implements UserService {

View File

@ -16,7 +16,7 @@ abstract class ListService {
Future<List<TaskList>> getAll();
Future<TaskList> get(int listId);
Future<List<TaskList>> getByNamespace(int namespaceId);
Future<TaskList> create(TaskList tl);
Future<TaskList> create(int namespaceId, TaskList tl);
Future<TaskList> update(TaskList tl);
Future delete(int listId);
}
@ -24,6 +24,7 @@ abstract class ListService {
abstract class TaskService {
Future<Task> update(Task task);
Future delete(int taskId);
Future<Task> add(int listId, Task task);
}
abstract class UserService {