Merge branch 'master' into feature/edit-task

# Conflicts:
#	pubspec.lock
#	pubspec.yaml
This commit is contained in:
kolaente 2019-03-18 18:15:17 +01:00
commit 822b1d5d8e
Signed by untrusted user: konrad
GPG Key ID: F40E70337AB24C9B
9 changed files with 322 additions and 339 deletions

View File

@ -33,12 +33,17 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
bool _loading = true;
User get currentUser => _currentUser;
Client get client => _client;
UserManager get userManager => new UserManager(_storage);
UserService newUserService(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
@ -72,6 +77,20 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
});
}
void logoutUser(BuildContext context) {
_storage.deleteAll().then((_) {
Navigator.pop(context);
setState(() {
_client = null;
_currentUser = null;
});
}).catchError((err) {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('An error occured while logging out!'),
));
});
}
void _loadCurrentUser() async {
var currentUser = await _storage.read(key: 'currentUser');
if (currentUser == null) {

View File

@ -12,7 +12,6 @@ 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(

View File

@ -25,7 +25,7 @@ class TaskList {
title = json['title'],
updated = DateTime.fromMillisecondsSinceEpoch(json['updated']),
created = DateTime.fromMillisecondsSinceEpoch(json['created']),
tasks = (json['tasks'] as List<dynamic>)
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();

View File

@ -1,9 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/GravatarImage.dart';
import 'package:vikunja_app/pages/namespace/namespace.dart';
import 'package:vikunja_app/pages/namespace/namespace_edit.dart';
import 'package:vikunja_app/pages/placeholder.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
@ -13,14 +16,118 @@ class HomePage extends StatefulWidget {
State<StatefulWidget> createState() => new HomePageState();
}
class HomePageState extends State<HomePage> {
class HomePageState extends State<HomePage> with AfterLayoutMixin<HomePage> {
List<Namespace> _namespaces = [];
Namespace get _currentNamespace =>
_selectedDrawerIndex >= 0 && _selectedDrawerIndex < _namespaces.length
? _namespaces[_selectedDrawerIndex]
: null;
int _selectedDrawerIndex = -1;
bool _loading = true;
bool _showUserDetails = false;
@override
void afterFirstLayout(BuildContext context) {
_loadNamespaces();
}
Widget _namespacesWidget() {
List<Widget> namespacesList = <Widget>[];
_namespaces
.asMap()
.forEach((i, namespace) => namespacesList.add(new ListTile(
leading: const Icon(Icons.folder),
title: new Text(namespace.name),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
return this._loading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
child: ListView(
padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context, tiles: namespacesList)
.toList()),
onRefresh: _loadNamespaces,
);
}
Widget _userDetailsWidget(BuildContext context) {
return ListView(padding: EdgeInsets.zero, children: <Widget>[
ListTile(
title: Text('Logout'),
leading: Icon(Icons.exit_to_app),
onTap: () {
VikunjaGlobal.of(context).logoutUser(context);
},
),
]);
}
@override
Widget build(BuildContext context) {
var currentUser = VikunjaGlobal.of(context).currentUser;
return new Scaffold(
appBar: AppBar(
title: new Text(_currentNamespace?.name ?? 'Vikunja'),
actions: _currentNamespace == null
? null
: <Widget>[
IconButton(
icon: Icon(Icons.edit),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => NamespaceEditPage(
namespace: _currentNamespace,
))))
],
),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail: currentUser == null ? null : Text(currentUser.email),
accountName: currentUser == null ? null : Text(currentUser.username),
onDetailsPressed: () {
setState(() {
_showUserDetails = !_showUserDetails;
});
},
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: GravatarImageProvider(currentUser.username)),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(
Theme.of(context).primaryColor, BlendMode.multiply)),
),
),
new Builder(
builder: (BuildContext context) => Expanded(
child: _showUserDetails
? _userDetailsWidget(context)
: _namespacesWidget())),
new Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
builder: (context) => ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespaceDialog(context),
),
),
),
])),
body: _getDrawerItemWidget(_selectedDrawerIndex),
);
}
_getDrawerItemWidget(int pos) {
if (pos == -1) {
@ -64,68 +171,4 @@ class HomePageState extends State<HomePage> {
});
});
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadNamespaces();
}
@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.name),
selected: i == _selectedDrawerIndex,
onTap: () => _onSelectItem(i),
)));
return new Scaffold(
appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vikunja')),
drawer: new Drawer(
child: new Column(children: <Widget>[
new UserAccountsDrawerHeader(
accountEmail: currentUser == null ? null : Text(currentUser.email),
accountName: currentUser == null ? null : Text(currentUser.username),
currentAccountPicture: currentUser == null
? null
: CircleAvatar(
backgroundImage: GravatarImageProvider(currentUser.username)),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("assets/graphics/hypnotize.png"),
repeat: ImageRepeat.repeat,
colorFilter: ColorFilter.mode(
Theme.of(context).primaryColor, BlendMode.multiply)),
),
),
new Expanded(
child: this._loading
? Center(child: CircularProgressIndicator())
: RefreshIndicator(
child: ListView(
padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context, tiles: drawerOptions)
.toList()),
onRefresh: _loadNamespaces,
)),
new Align(
alignment: FractionalOffset.bottomCenter,
child: Builder(
builder: (context) => ListTile(
leading: const Icon(Icons.add),
title: const Text('Add namespace...'),
onTap: () => _addNamespaceDialog(context),
),
),
),
])),
body: _getDrawerItemWidget(_selectedDrawerIndex),
);
}
}

View File

@ -54,12 +54,14 @@ class _ListPageState extends State<ListPage> {
),
body: !this._loading
? RefreshIndicator(
child: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context, tiles: _listTasks())
.toList(),
),
child: _list.tasks.length > 0
? ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context, tiles: _listTasks())
.toList(),
)
: Center(child: Text('This list is empty.')),
onRefresh: _loadList,
)
: Center(child: CircularProgressIndicator()),
@ -109,10 +111,10 @@ class _ListPageState extends State<ListPage> {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((tasks) {
.then((list) {
setState(() {
_loading = false;
_list = tasks;
_list = list;
});
});
}

View File

@ -2,6 +2,8 @@ import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
@ -17,10 +19,16 @@ class NamespacePage extends StatefulWidget {
_NamespacePageState createState() => new _NamespacePageState();
}
class _NamespacePageState extends State<NamespacePage> {
class _NamespacePageState extends State<NamespacePage>
with AfterLayoutMixin<NamespacePage> {
List<TaskList> _lists = [];
bool _loading = true;
@override
void afterFirstLayout(BuildContext context) {
_loadLists();
}
/////
// This essentially shows the lists.
@override
@ -28,32 +36,36 @@ class _NamespacePageState extends State<NamespacePage> {
return Scaffold(
body: !this._loading
? RefreshIndicator(
child: 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(),
),
onRefresh: _updateLists,
child: _lists.length > 0
? 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(),
)
: Center(child: Text('This namespace is empty.')),
onRefresh: _loadLists,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: Builder(
@ -66,17 +78,17 @@ class _NamespacePageState extends State<NamespacePage> {
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateLists();
_loadLists();
}
Future _removeList(TaskList list) {
return VikunjaGlobal.of(context)
.listService
.delete(list.id)
.then((_) => _updateLists());
.then((_) => _loadLists());
}
Future<void> _updateLists() {
Future<void> _loadLists() {
return VikunjaGlobal.of(context)
.listService
.getByNamespace(widget.namespace.id)
@ -107,7 +119,7 @@ class _NamespacePageState extends State<NamespacePage> {
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: []))
.then((_) {
setState(() {});
_updateLists();
_loadLists();
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('The list was successfully created!'),

View File

@ -0,0 +1,125 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/theme/button.dart';
import 'package:vikunja_app/theme/buttonText.dart';
class NamespaceEditPage extends StatefulWidget {
final Namespace namespace;
NamespaceEditPage({this.namespace}) : super(key: Key(namespace.toString()));
@override
State<StatefulWidget> createState() => _NamespaceEditPageState();
}
class _NamespaceEditPageState extends State<NamespaceEditPage> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String _name, _description;
@override
Widget build(BuildContext ctx) {
return Scaffold(
appBar: AppBar(
title: Text('Edit Namespace'),
),
body: Builder(
builder: (BuildContext context) => SafeArea(
child: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16.0),
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.namespace.name,
onSaved: (name) => _name = name,
validator: (name) {
if (name.length < 3 || name.length > 250) {
return 'The name needs to have between 3 and 250 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Name',
border: OutlineInputBorder(),
),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: TextFormField(
maxLines: null,
keyboardType: TextInputType.multiline,
initialValue: widget.namespace.description,
onSaved: (description) => _description = description,
validator: (description) {
if (description.length > 1000) {
return 'The description can have a maximum of 1000 characters.';
}
return null;
},
decoration: new InputDecoration(
labelText: 'Description',
border: OutlineInputBorder(),
),
),
),
Builder(
builder: (context) => Padding(
padding: EdgeInsets.symmetric(vertical: 10.0),
child: FancyButton(
onPressed: !_loading
? () {
if (_formKey.currentState.validate()) {
Form.of(context).save();
_saveNamespace(context);
}
}
: null,
child: _loading
? CircularProgressIndicator()
: VikunjaButtonText('Save'),
))),
]),
),
),
),
);
}
_saveNamespace(BuildContext context) async {
setState(() => _loading = true);
// FIXME: is there a way we can update the namespace without creating a new namespace object?
// aka updating the existing namespace we got from context (setters?)
Namespace updatedNamespace = Namespace(
id: widget.namespace.id,
name: _name,
description: _description,
owner: widget.namespace.owner);
VikunjaGlobal.of(context)
.namespaceService
.update(updatedNamespace)
.then((_) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The namespace was updated successfully!'),
));
}).catchError((err) {
setState(() => _loading = false);
Scaffold.of(context).showSnackBar(
SnackBar(
content: Text('Something went wrong: ' + err.toString()),
action: SnackBarAction(
label: 'CLOSE',
onPressed: Scaffold.of(context).hideCurrentSnackBar),
),
);
});
}
}

View File

@ -1,13 +1,13 @@
# Generated by pub
# See https://www.dartlang.org/tools/pub/glossary#lockfile
packages:
analyzer:
dependency: transitive
after_layout:
dependency: "direct main"
description:
name: analyzer
name: after_layout
url: "https://pub.dartlang.org"
source: hosted
version: "0.34.3"
version: "1.0.7"
archive:
dependency: transitive
description:
@ -64,13 +64,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.6"
csslib:
dependency: transitive
description:
name: csslib
url: "https://pub.dartlang.org"
source: hosted
version: "0.14.6"
cupertino_icons:
dependency: "direct main"
description:
@ -87,13 +80,6 @@ packages:
url: "https://github.com/MarkOSullivan94/dart_config.git"
source: git
version: "0.5.0"
datetime_picker_formfield:
dependency: "direct main"
description:
name: datetime_picker_formfield
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.8"
flutter:
dependency: "direct main"
description: flutter
@ -118,27 +104,6 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
front_end:
dependency: transitive
description:
name: front_end
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.9+1"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.7"
html:
dependency: transitive
description:
name: html
url: "https://pub.dartlang.org"
source: hosted
version: "0.13.4+1"
http:
dependency: "direct main"
description:
@ -146,13 +111,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.5"
http_parser:
dependency: transitive
description:
@ -167,48 +125,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.7"
intl:
dependency: transitive
description:
name: intl
url: "https://pub.dartlang.org"
source: hosted
version: "0.15.7"
io:
dependency: transitive
description:
name: io
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.3"
js:
dependency: transitive
description:
name: js
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.1+1"
json_rpc_2:
dependency: transitive
description:
name: json_rpc_2
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.9"
kernel:
dependency: transitive
description:
name: kernel
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.9+1"
logging:
dependency: transitive
description:
name: logging
url: "https://pub.dartlang.org"
source: hosted
version: "0.11.3+2"
matcher:
dependency: transitive
description:
@ -223,41 +139,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
mime:
dependency: transitive
description:
name: mime
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.6+2"
multi_server_socket:
dependency: transitive
description:
name: multi_server_socket
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.2"
node_preamble:
dependency: transitive
description:
name: node_preamble
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.4"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.5"
package_resolver:
dependency: transitive
description:
name: package_resolver
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.9"
path:
dependency: transitive
description:
@ -265,6 +146,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.6.2"
pedantic:
dependency: transitive
description:
name: pedantic
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
petitparser:
dependency: transitive
description:
@ -272,27 +160,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
plugin:
dependency: transitive
description:
name: plugin
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+3"
pool:
dependency: transitive
description:
name: pool
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.0"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.2"
quiver:
dependency: transitive
description:
@ -300,60 +167,18 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
shelf:
dependency: transitive
description:
name: shelf
url: "https://pub.dartlang.org"
source: hosted
version: "0.7.4"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
shelf_static:
dependency: transitive
description:
name: shelf_static
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.8"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.2+4"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.5"
source_maps:
dependency: transitive
description:
name: source_maps
url: "https://pub.dartlang.org"
source: hosted
version: "0.10.8"
source_span:
dependency: transitive
description:
name: source_span
url: "https://pub.dartlang.org"
source: hosted
version: "1.4.1"
version: "1.5.4"
stack_trace:
dependency: transitive
description:
@ -381,28 +206,14 @@ packages:
name: term_glyph
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
test:
dependency: "direct dev"
description:
name: test
url: "https://pub.dartlang.org"
source: hosted
version: "1.5.1+1"
version: "1.1.0"
test_api:
dependency: transitive
description:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.1"
test_core:
dependency: transitive
description:
name: test_core
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.0+1"
version: "0.2.2"
typed_data:
dependency: transitive
description:
@ -410,13 +221,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.6"
utf:
dependency: transitive
description:
name: utf
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.0+5"
vector_math:
dependency: transitive
description:
@ -424,27 +228,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.8"
vm_service_client:
dependency: transitive
description:
name: vm_service_client
url: "https://pub.dartlang.org"
source: hosted
version: "0.2.6"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "0.9.7+10"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.9"
xml:
dependency: transitive
description:
@ -460,5 +243,4 @@ packages:
source: hosted
version: "2.1.15"
sdks:
dart: ">=2.1.0-dev.5.0 <3.0.0"
flutter: ">=0.1.2 <2.0.0"
dart: ">=2.1.0 <3.0.0"

View File

@ -12,6 +12,7 @@ dependencies:
cupertino_icons: ^0.1.2
flutter_secure_storage: 3.1.1
http: 0.12.0
after_layout: ^1.0.7
datetime_picker_formfield: ^0.1.8
dev_dependencies: