From 08fa9167a27e00145c18f4e58da5fc6ffef671d5 Mon Sep 17 00:00:00 2001 From: JonasFranz Date: Thu, 27 Sep 2018 15:55:56 +0000 Subject: [PATCH] Add improved loading indicators (#9) --- lib/components/AddDialog.dart | 39 +++++++++ lib/components/TaskTile.dart | 89 ++++++++++++++++++++ lib/fragments/namespace.dart | 99 ++++++++++------------ lib/pages/home_page.dart | 59 +++++-------- lib/pages/list_page.dart | 154 ++++++++++++++++------------------ lib/pages/login_page.dart | 1 - 6 files changed, 262 insertions(+), 179 deletions(-) create mode 100644 lib/components/AddDialog.dart create mode 100644 lib/components/TaskTile.dart diff --git a/lib/components/AddDialog.dart b/lib/components/AddDialog.dart new file mode 100644 index 0000000..6cb56ca --- /dev/null +++ b/lib/components/AddDialog.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class AddDialog extends StatelessWidget { + final ValueChanged 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: [ + Expanded( + child: new TextField( + autofocus: true, + decoration: this.decoration, + controller: textController, + ), + ) + ]), + actions: [ + 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); + }, + ) + ], + ); + } +} diff --git a/lib/components/TaskTile.dart b/lib/components/TaskTile.dart new file mode 100644 index 0000000..470f437 --- /dev/null +++ b/lib/components/TaskTile.dart @@ -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 { + 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 _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 TaskChanged(Task task, bool newValue); diff --git a/lib/fragments/namespace.dart b/lib/fragments/namespace.dart index e7b0f1b..afde431 100644 --- a/lib/fragments/namespace.dart +++ b/lib/fragments/namespace.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:vikunja_app/components/AddDialog.dart'; import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/models/namespace.dart'; import 'package:vikunja_app/models/task.dart'; @@ -18,35 +19,41 @@ class NamespaceFragment extends StatefulWidget { class _NamespaceFragmentState extends State { List _lists = []; + bool _loading = true; @override Widget build(BuildContext context) { 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(), - ), + 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, + ) + : Center(child: CircularProgressIndicator()), floatingActionButton: FloatingActionButton( onPressed: () => _addListDialog(), child: const Icon(Icons.add)), ); @@ -65,11 +72,14 @@ class _NamespaceFragmentState extends State { .then((_) => _updateLists()); } - _updateLists() { - VikunjaGlobal.of(context) + Future _updateLists() { + return VikunjaGlobal.of(context) .listService .getByNamespace(widget.namespace.id) - .then((lists) => setState(() => this._lists = lists)); + .then((lists) => setState(() { + this._lists = lists; + this._loading = false; + })); } _openList(BuildContext context, TaskList list) { @@ -78,37 +88,12 @@ class _NamespaceFragmentState extends State { } _addListDialog() { - var textController = new TextEditingController(); showDialog( context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row(children: [ - Expanded( - child: new TextField( - autofocus: true, - decoration: new InputDecoration( - labelText: 'List Name', hintText: 'eg. Shopping List'), - controller: textController, - ), - ) - ]), - actions: [ - 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); - }, - ) - ], - ), + builder: (_) => AddDialog( + onAdd: _addList, + decoration: new InputDecoration( + labelText: 'List Name', hintText: 'eg. Shopping List')), ); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 98e74bc..05e2e17 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,4 +1,7 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; +import 'package:vikunja_app/components/AddDialog.dart'; import 'package:vikunja_app/components/GravatarImage.dart'; import 'package:vikunja_app/fragments/namespace.dart'; import 'package:vikunja_app/fragments/placeholder.dart'; @@ -19,6 +22,7 @@ class HomePageState extends State { ? _namespaces[_selectedDrawerIndex] : null; int _selectedDrawerIndex = -1; + bool _loading = true; _getDrawerItemWidget(int pos) { if (pos == -1) { @@ -33,38 +37,13 @@ class HomePageState extends State { } _addNamespaceDialog() { - var textController = new TextEditingController(); showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row(children: [ - Expanded( - child: new TextField( - autofocus: true, + context: context, + builder: (_) => AddDialog( + onAdd: _addNamespace, decoration: new InputDecoration( - labelText: 'Namespace', hintText: 'eg. Family Namespace'), - controller: textController, - ), - ) - ]), - actions: [ - 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); - }, - ) - ], - ), - ); + labelText: 'Namespace', hintText: 'eg. Personal Namespace'), + )); } _addNamespace(String name) { @@ -74,9 +53,10 @@ class HomePageState extends State { .then((_) => _updateNamespaces()); } - _updateNamespaces() { - VikunjaGlobal.of(context).namespaceService.getAll().then((result) { + Future _updateNamespaces() { + return VikunjaGlobal.of(context).namespaceService.getAll().then((result) { setState(() { + _loading = false; _namespaces = result; }); }); @@ -121,11 +101,16 @@ class HomePageState extends State { ), ), new Expanded( - child: ListView( - padding: EdgeInsets.zero, - children: - ListTile.divideTiles(context: context, tiles: drawerOptions) - .toList())), + child: this._loading + ? Center(child: CircularProgressIndicator()) + : RefreshIndicator( + child: ListView( + padding: EdgeInsets.zero, + children: ListTile.divideTiles( + context: context, tiles: drawerOptions) + .toList()), + onRefresh: _updateNamespaces, + )), new Align( alignment: FractionalOffset.bottomCenter, child: new ListTile( diff --git a/lib/pages/list_page.dart b/lib/pages/list_page.dart index 50661b6..4ff9a96 100644 --- a/lib/pages/list_page.dart +++ b/lib/pages/list_page.dart @@ -1,4 +1,8 @@ +import 'dart:async'; + 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/models/task.dart'; @@ -12,117 +16,99 @@ class ListPage extends StatefulWidget { } class _ListPageState extends State { - TaskList items; + TaskList _items; + List _loadingTasks = []; + bool _loading = true; @override void initState() { - items = TaskList( + _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(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 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()); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: new Text(_items.title), + actions: [ + IconButton( + icon: Icon(Icons.edit), + onPressed: () => {/* TODO add edit list functionality */}, + ) + ], + ), + 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() { - VikunjaGlobal.of(context).listService.get(widget.taskList.id).then((tasks) { + List _listTasks() { + 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 _updateList() { + return VikunjaGlobal.of(context) + .listService + .get(widget.taskList.id) + .then((tasks) { setState(() { - items = tasks; + _loading = false; + _items = tasks; }); }); } _addItemDialog() { - var textController = new TextEditingController(); showDialog( - context: context, - child: new AlertDialog( - contentPadding: const EdgeInsets.all(16.0), - content: new Row(children: [ - Expanded( - child: new TextField( - autofocus: true, - decoration: new InputDecoration( - labelText: 'List Item', hintText: 'eg. Milk'), - controller: textController, - ), - ) - ]), - actions: [ - 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); - }, - ) - ], - ), - ); + context: context, + builder: (_) => AddDialog( + onAdd: _addItem, + decoration: new InputDecoration( + labelText: 'List Item', hintText: 'eg. Milk'))); } _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) { + var newTask = + Task(id: null, text: name, owner: globalState.currentUser, done: false); + setState(() => _loadingTasks.add(newTask)); + globalState.taskService.add(_items.id, newTask).then((task) { setState(() { - items.tasks.add(task); + _items.tasks.add(task); }); - }).then((_) => _updateList()); + }).then((_) => _updateList() + .then((_) => setState(() => _loadingTasks.remove(newTask)))); } } diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 307245d..8db0f52 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -99,7 +99,6 @@ class _LoginPageState extends State { await vGlobal.newLoginService(_server).login(_username, _password); vGlobal.changeUser(newUser.user, token: newUser.token, base: _server); } catch (ex) { - print(ex); showDialog( context: context, builder: (context) => new AlertDialog(