Add improved loading indicators (#9)

This commit is contained in:
JonasFranz 2018-09-27 15:55:56 +00:00 committed by Gitea
parent b87a9a4633
commit 08fa9167a2
6 changed files with 262 additions and 179 deletions

View File

@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
class AddDialog extends StatelessWidget {
final ValueChanged<String> onAdd;
final InputDecoration decoration;
const AddDialog({Key key, this.onAdd, this.decoration}) : super(key: key);
@override
Widget build(BuildContext context) {
var textController = TextEditingController();
return new AlertDialog(
contentPadding: const EdgeInsets.all(16.0),
content: new Row(children: <Widget>[
Expanded(
child: new TextField(
autofocus: true,
decoration: this.decoration,
controller: textController,
),
)
]),
actions: <Widget>[
new FlatButton(
child: const Text('CANCEL'),
onPressed: () => Navigator.pop(context),
),
new FlatButton(
child: const Text('ADD'),
onPressed: () {
if (this.onAdd != null && textController.text.isNotEmpty)
this.onAdd(textController.text);
Navigator.pop(context);
},
)
],
);
}
}

View File

@ -0,0 +1,89 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/task.dart';
class TaskTile extends StatefulWidget {
final Task task;
final VoidCallback onEdit;
final bool loading;
const TaskTile(
{Key key, @required this.task, this.onEdit, this.loading = false})
: assert(task != null),
super(key: key);
@override
TaskTileState createState() {
return new TaskTileState(this.task, this.loading);
}
}
class TaskTileState extends State<TaskTile> {
bool _loading;
Task _currentTask;
TaskTileState(this._currentTask, this._loading)
: assert(_currentTask != null),
assert(_loading != null);
@override
Widget build(BuildContext context) {
if (_loading) {
return ListTile(
leading: Padding(
padding: const EdgeInsets.all(8.0),
child: SizedBox(
height: Checkbox.width,
width: Checkbox.width,
child: CircularProgressIndicator(
strokeWidth: 2.0,
)),
),
title: Text(_currentTask.text),
subtitle:
_currentTask.description == null || _currentTask.description.isEmpty
? null
: Text(_currentTask.description),
trailing: IconButton(icon: Icon(Icons.settings), onPressed: null),
);
}
return CheckboxListTile(
title: Text(_currentTask.text),
controlAffinity: ListTileControlAffinity.leading,
value: _currentTask.done ?? false,
subtitle:
_currentTask.description == null || _currentTask.description.isEmpty
? null
: Text(_currentTask.description),
secondary:
IconButton(icon: Icon(Icons.settings), onPressed: widget.onEdit),
onChanged: _change,
);
}
void _change(bool value) async {
setState(() {
this._loading = true;
});
Task newTask = await _updateTask(_currentTask, value);
setState(() {
this._currentTask = newTask;
this._loading = false;
});
}
Future<Task> _updateTask(Task task, bool checked) {
// TODO use copyFrom
return VikunjaGlobal.of(context).taskService.update(Task(
id: task.id,
done: checked,
text: task.text,
description: task.description,
owner: null,
));
}
}
typedef Future<void> TaskChanged(Task task, bool newValue);

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/namespace.dart'; import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/task.dart';
@ -18,35 +19,41 @@ class NamespaceFragment extends StatefulWidget {
class _NamespaceFragmentState extends State<NamespaceFragment> { class _NamespaceFragmentState extends State<NamespaceFragment> {
List<TaskList> _lists = []; List<TaskList> _lists = [];
bool _loading = true;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: new ListView( body: !this._loading
padding: EdgeInsets.symmetric(vertical: 8.0), ? RefreshIndicator(
children: ListTile.divideTiles( child: new ListView(
context: context, padding: EdgeInsets.symmetric(vertical: 8.0),
tiles: _lists.map((ls) => Dismissible( children: ListTile.divideTiles(
key: Key(ls.id.toString()), context: context,
direction: DismissDirection.startToEnd, tiles: _lists.map((ls) => Dismissible(
child: ListTile( key: Key(ls.id.toString()),
title: new Text(ls.title), direction: DismissDirection.startToEnd,
onTap: () => _openList(context, ls), child: ListTile(
trailing: Icon(Icons.arrow_right), title: new Text(ls.title),
), onTap: () => _openList(context, ls),
background: Container( trailing: Icon(Icons.arrow_right),
color: Colors.red, ),
child: const ListTile( background: Container(
leading: Icon(Icons.delete, color: Colors.red,
color: Colors.white, size: 36.0)), child: const ListTile(
), leading: Icon(Icons.delete,
onDismissed: (direction) { color: Colors.white, size: 36.0)),
_removeList(ls).then((_) => Scaffold.of(context) ),
.showSnackBar( onDismissed: (direction) {
SnackBar(content: Text("${ls.title} removed")))); _removeList(ls).then((_) => Scaffold.of(context)
}, .showSnackBar(SnackBar(
))).toList(), content: Text("${ls.title} removed"))));
), },
))).toList(),
),
onRefresh: _updateLists,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => _addListDialog(), child: const Icon(Icons.add)), onPressed: () => _addListDialog(), child: const Icon(Icons.add)),
); );
@ -65,11 +72,14 @@ class _NamespaceFragmentState extends State<NamespaceFragment> {
.then((_) => _updateLists()); .then((_) => _updateLists());
} }
_updateLists() { Future<void> _updateLists() {
VikunjaGlobal.of(context) return VikunjaGlobal.of(context)
.listService .listService
.getByNamespace(widget.namespace.id) .getByNamespace(widget.namespace.id)
.then((lists) => setState(() => this._lists = lists)); .then((lists) => setState(() {
this._lists = lists;
this._loading = false;
}));
} }
_openList(BuildContext context, TaskList list) { _openList(BuildContext context, TaskList list) {
@ -78,37 +88,12 @@ class _NamespaceFragmentState extends State<NamespaceFragment> {
} }
_addListDialog() { _addListDialog() {
var textController = new TextEditingController();
showDialog( showDialog(
context: context, context: context,
child: new AlertDialog( builder: (_) => AddDialog(
contentPadding: const EdgeInsets.all(16.0), onAdd: _addList,
content: new Row(children: <Widget>[ decoration: new InputDecoration(
Expanded( labelText: 'List Name', hintText: 'eg. Shopping List')),
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);
},
)
],
),
); );
} }

View File

@ -1,4 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/GravatarImage.dart'; import 'package:vikunja_app/components/GravatarImage.dart';
import 'package:vikunja_app/fragments/namespace.dart'; import 'package:vikunja_app/fragments/namespace.dart';
import 'package:vikunja_app/fragments/placeholder.dart'; import 'package:vikunja_app/fragments/placeholder.dart';
@ -19,6 +22,7 @@ class HomePageState extends State<HomePage> {
? _namespaces[_selectedDrawerIndex] ? _namespaces[_selectedDrawerIndex]
: null; : null;
int _selectedDrawerIndex = -1; int _selectedDrawerIndex = -1;
bool _loading = true;
_getDrawerItemWidget(int pos) { _getDrawerItemWidget(int pos) {
if (pos == -1) { if (pos == -1) {
@ -33,38 +37,13 @@ class HomePageState extends State<HomePage> {
} }
_addNamespaceDialog() { _addNamespaceDialog() {
var textController = new TextEditingController();
showDialog( showDialog(
context: context, context: context,
child: new AlertDialog( builder: (_) => AddDialog(
contentPadding: const EdgeInsets.all(16.0), onAdd: _addNamespace,
content: new Row(children: <Widget>[
Expanded(
child: new TextField(
autofocus: true,
decoration: new InputDecoration( decoration: new InputDecoration(
labelText: 'Namespace', hintText: 'eg. Family Namespace'), labelText: 'Namespace', hintText: 'eg. Personal Namespace'),
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) {
_addNamespace(textController.text);
}
Navigator.pop(context);
},
)
],
),
);
} }
_addNamespace(String name) { _addNamespace(String name) {
@ -74,9 +53,10 @@ class HomePageState extends State<HomePage> {
.then((_) => _updateNamespaces()); .then((_) => _updateNamespaces());
} }
_updateNamespaces() { Future<void> _updateNamespaces() {
VikunjaGlobal.of(context).namespaceService.getAll().then((result) { return VikunjaGlobal.of(context).namespaceService.getAll().then((result) {
setState(() { setState(() {
_loading = false;
_namespaces = result; _namespaces = result;
}); });
}); });
@ -121,11 +101,16 @@ class HomePageState extends State<HomePage> {
), ),
), ),
new Expanded( new Expanded(
child: ListView( child: this._loading
padding: EdgeInsets.zero, ? Center(child: CircularProgressIndicator())
children: : RefreshIndicator(
ListTile.divideTiles(context: context, tiles: drawerOptions) child: ListView(
.toList())), padding: EdgeInsets.zero,
children: ListTile.divideTiles(
context: context, tiles: drawerOptions)
.toList()),
onRefresh: _updateNamespaces,
)),
new Align( new Align(
alignment: FractionalOffset.bottomCenter, alignment: FractionalOffset.bottomCenter,
child: new ListTile( child: new ListTile(

View File

@ -1,4 +1,8 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/TaskTile.dart';
import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/task.dart';
@ -12,117 +16,99 @@ class ListPage extends StatefulWidget {
} }
class _ListPageState extends State<ListPage> { class _ListPageState extends State<ListPage> {
TaskList items; TaskList _items;
List<Task> _loadingTasks = [];
bool _loading = true;
@override @override
void initState() { void initState() {
items = TaskList( _items = TaskList(
id: widget.taskList.id, title: widget.taskList.title, tasks: []); id: widget.taskList.id, title: widget.taskList.title, tasks: []);
super.initState(); super.initState();
} }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: new Text(items.title),
),
body: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context,
tiles: items?.tasks?.map((task) => CheckboxListTile(
title: Text(task.text),
controlAffinity: ListTileControlAffinity.leading,
value: task.done ?? false,
subtitle: task.description == null
? null
: Text(task.description),
onChanged: (bool value) => _updateTask(task, value),
)) ??
[])
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addItemDialog(), child: Icon(Icons.add)),
);
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
super.didChangeDependencies(); super.didChangeDependencies();
_updateList(); _updateList();
} }
_updateTask(Task task, bool checked) { @override
// TODO use copyFrom Widget build(BuildContext context) {
VikunjaGlobal.of(context) return Scaffold(
.taskService appBar: AppBar(
.update(Task( title: new Text(_items.title),
id: task.id, actions: <Widget>[
done: checked, IconButton(
text: task.text, icon: Icon(Icons.edit),
description: task.description, onPressed: () => {/* TODO add edit list functionality */},
owner: null, )
)) ],
.then((_) => _updateList()); ),
body: !this._loading
? RefreshIndicator(
child: ListView(
padding: EdgeInsets.symmetric(vertical: 8.0),
children:
ListTile.divideTiles(context: context, tiles: _listTasks())
.toList(),
),
onRefresh: _updateList,
)
: Center(child: CircularProgressIndicator()),
floatingActionButton: FloatingActionButton(
onPressed: () => _addItemDialog(), child: Icon(Icons.add)),
);
} }
_updateList() { List<Widget> _listTasks() {
VikunjaGlobal.of(context).listService.get(widget.taskList.id).then((tasks) { var tasks = (_items?.tasks?.map(_buildTile) ?? []).toList();
tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
}
TaskTile _buildTile(Task task) {
return TaskTile(task: task, loading: false);
}
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true,
);
}
Future<void> _updateList() {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((tasks) {
setState(() { setState(() {
items = tasks; _loading = false;
_items = tasks;
}); });
}); });
} }
_addItemDialog() { _addItemDialog() {
var textController = new TextEditingController();
showDialog( showDialog(
context: context, context: context,
child: new AlertDialog( builder: (_) => AddDialog(
contentPadding: const EdgeInsets.all(16.0), onAdd: _addItem,
content: new Row(children: <Widget>[ decoration: new InputDecoration(
Expanded( labelText: 'List Item', hintText: 'eg. Milk')));
child: new TextField(
autofocus: true,
decoration: new InputDecoration(
labelText: 'List Item', hintText: 'eg. Milk'),
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) _addItem(textController.text);
Navigator.pop(context);
},
)
],
),
);
} }
_addItem(String name) { _addItem(String name) {
var globalState = VikunjaGlobal.of(context); var globalState = VikunjaGlobal.of(context);
globalState.taskService var newTask =
.add( Task(id: null, text: name, owner: globalState.currentUser, done: false);
items.id, setState(() => _loadingTasks.add(newTask));
Task( globalState.taskService.add(_items.id, newTask).then((task) {
id: null,
text: name,
owner: globalState.currentUser,
done: false))
.then((task) {
setState(() { setState(() {
items.tasks.add(task); _items.tasks.add(task);
}); });
}).then((_) => _updateList()); }).then((_) => _updateList()
.then((_) => setState(() => _loadingTasks.remove(newTask))));
} }
} }

View File

@ -99,7 +99,6 @@ class _LoginPageState extends State<LoginPage> {
await vGlobal.newLoginService(_server).login(_username, _password); await vGlobal.newLoginService(_server).login(_username, _password);
vGlobal.changeUser(newUser.user, token: newUser.token, base: _server); vGlobal.changeUser(newUser.user, token: newUser.token, base: _server);
} catch (ex) { } catch (ex) {
print(ex);
showDialog( showDialog(
context: context, context: context,
builder: (context) => new AlertDialog( builder: (context) => new AlertDialog(