From 4a29480ded98b7a897c15b30bd4cf38e947b942e Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 23 Sep 2018 15:38:02 +0000 Subject: [PATCH 1/9] added fdroid flavour (#3) --- android/app/build.gradle | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index f94225f..ed3a151 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -44,6 +44,24 @@ android { versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + } + + flavorDimensions "deploy" + + productFlavors { + fdroid { + dimension "deploy" + signingConfig null + } + } + + android.applicationVariants.all { variant -> + if (variant.flavorName == "fdroid") { + variant.outputs.all { output -> + output.outputFileName = "app-fdroid-release.apk" + } + } } buildTypes { From cabac017d9f2462df3fad8a89e54e3363d5fcbce Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 23 Sep 2018 16:04:00 +0000 Subject: [PATCH 2/9] Fixed build (#4) --- .drone.yml | 2 +- Makefile | 6 +++--- android/app/build.gradle | 10 +++++++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.drone.yml b/.drone.yml index 6f0fd26..a504da8 100644 --- a/.drone.yml +++ b/.drone.yml @@ -23,7 +23,7 @@ pipeline: - flutter packages get - make build-all - mkdir apks - - mv build/app/outputs/apk/*/*.apk apks + - mv build/app/outputs/apk/*/*/*.apk apks when: event: [ push, tag ] diff --git a/Makefile b/Makefile index 681101e..b14bbd2 100644 --- a/Makefile +++ b/Makefile @@ -19,12 +19,12 @@ build-all: build-release build-debug build-profile .PHONY: build-release build-release: - flutter build apk --release --build-name=$(VERSION) + flutter build apk --release --build-name=$(VERSION) --flavor main .PHONY: build-debug build-debug: - flutter build apk --debug --build-name=$(VERSION) + flutter build apk --debug --build-name=$(VERSION) --flavor unsigned .PHONY: build-profile build-profile: - flutter build apk --profile --build-name=$(VERSION) \ No newline at end of file + flutter build apk --profile --build-name=$(VERSION) --flavor unsigned diff --git a/android/app/build.gradle b/android/app/build.gradle index ed3a151..f8df111 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -38,7 +38,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.vikunja.flutteringvikunja" + applicationId "io.vikunja.app" minSdkVersion 18 targetSdkVersion 27 versionCode flutterVersionCode.toInteger() @@ -54,6 +54,14 @@ android { dimension "deploy" signingConfig null } + unsigned { + dimension "deploy" + signingConfig null + } + main { + dimension "deploy" + signingConfig signingConfigs.debug // TODO add signing key + } } android.applicationVariants.all { variant -> From 3cb375dafefe2da2532a4d6e67174c854c58b6fe Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 23 Sep 2018 16:23:53 +0000 Subject: [PATCH 3/9] Removed .idea folder (#7) --- .idea/runConfigurations/main_dart.xml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .idea/runConfigurations/main_dart.xml diff --git a/.idea/runConfigurations/main_dart.xml b/.idea/runConfigurations/main_dart.xml deleted file mode 100644 index aab7b5c..0000000 --- a/.idea/runConfigurations/main_dart.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - \ No newline at end of file From d70863a75265c333be657a6d6a487af32f92e4db Mon Sep 17 00:00:00 2001 From: konrad Date: Sun, 23 Sep 2018 16:39:20 +0000 Subject: [PATCH 4/9] Fixed version (#8) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 96eb8d8..de541af 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: vikunja_app description: Vikunja as Flutter cross platform app -version: 1.0.0+1 +version: 0.0.1 environment: sdk: ">=2.0.0-dev.63.0 <3.0.0" From 3c5293153872406cce40513dd4f66b0a5e1c0438 Mon Sep 17 00:00:00 2001 From: konrad Date: Tue, 25 Sep 2018 12:06:41 +0000 Subject: [PATCH 5/9] Fixed url regex (#11) --- lib/pages/login_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 88bbc4a..307245d 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -8,7 +8,7 @@ class LoginPage extends StatefulWidget { } final RegExp _url = new RegExp( - r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)'); + r'https?:\/\/[a-zA-Z0-9.]+(:[0-9]+)?'); class _LoginPageState extends State { final _formKey = GlobalKey(); From 2e04969689248b420ad880be88fd951336bde012 Mon Sep 17 00:00:00 2001 From: JonasFranz Date: Thu, 27 Sep 2018 15:55:56 +0000 Subject: [PATCH 6/9] 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( From 8e90f9b17071fff52b1582ded550839709ffbdd4 Mon Sep 17 00:00:00 2001 From: konrad Date: Thu, 27 Sep 2018 16:04:37 +0000 Subject: [PATCH 7/9] Improved regex (#12) --- lib/pages/login_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 8db0f52..17bbd33 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -8,7 +8,7 @@ class LoginPage extends StatefulWidget { } final RegExp _url = new RegExp( - r'https?:\/\/[a-zA-Z0-9.]+(:[0-9]+)?'); + r'https?:\/\/((([a-zA-Z0-9.\-\_]+)\.[a-zA-Z]+)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?'); class _LoginPageState extends State { final _formKey = GlobalKey(); From 301997f32b3a53836a5f76f4bb49039c340d1624 Mon Sep 17 00:00:00 2001 From: konrad Date: Tue, 2 Oct 2018 15:53:57 +0000 Subject: [PATCH 8/9] Fix Typo (#14) --- lib/pages/home_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 05e2e17..471aa79 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -82,7 +82,7 @@ class HomePageState extends State { ))); return new Scaffold( - appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vakunja')), + appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vikunja')), drawer: new Drawer( child: new Column(children: [ new UserAccountsDrawerHeader( From abf0196de31648b2db6383b10ab57831c7689140 Mon Sep 17 00:00:00 2001 From: konrad Date: Mon, 8 Oct 2018 14:26:01 +0000 Subject: [PATCH 9/9] Added register (#13) --- lib/api/user_implementation.dart | 10 ++ lib/global.dart | 3 +- lib/pages/login_page.dart | 24 +++-- lib/pages/register_page.dart | 152 +++++++++++++++++++++++++++++++ lib/service/mocked_services.dart | 5 + lib/service/services.dart | 1 + lib/utils/validator.dart | 13 +++ 7 files changed, 199 insertions(+), 9 deletions(-) create mode 100644 lib/pages/register_page.dart create mode 100644 lib/utils/validator.dart diff --git a/lib/api/user_implementation.dart b/lib/api/user_implementation.dart index 61f3a99..a07bb2e 100644 --- a/lib/api/user_implementation.dart +++ b/lib/api/user_implementation.dart @@ -19,6 +19,16 @@ class UserAPIService extends APIService implements UserService { .then((user) => UserTokenPair(user, token)); } + @override + Future register(String username, email, password) async { + var newUser = await client.post('/register', body: { + 'username': username, + 'email': email, + 'password': password + }).then((resp) => resp['username']); + return login(newUser, password); + } + @override Future getCurrentUser() { return client.get('/user').then((map) => User.fromJson(map)); diff --git a/lib/global.dart b/lib/global.dart index f75de00..622ef19 100644 --- a/lib/global.dart +++ b/lib/global.dart @@ -37,8 +37,7 @@ class VikunjaGlobalState extends State { Client get client => _client; UserManager get userManager => new UserManager(_storage); - UserService get userService => new UserAPIService(_client); - UserService newLoginService(base) => new UserAPIService(Client(null, base)); + 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); diff --git a/lib/pages/login_page.dart b/lib/pages/login_page.dart index 17bbd33..eb65be0 100644 --- a/lib/pages/login_page.dart +++ b/lib/pages/login_page.dart @@ -1,15 +1,13 @@ import 'package:flutter/material.dart'; import 'package:vikunja_app/global.dart'; -import 'package:vikunja_app/main.dart'; +import 'package:vikunja_app/pages/register_page.dart'; +import 'package:vikunja_app/utils/validator.dart'; class LoginPage extends StatefulWidget { @override _LoginPageState createState() => _LoginPageState(); } -final RegExp _url = new RegExp( - r'https?:\/\/((([a-zA-Z0-9.\-\_]+)\.[a-zA-Z]+)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?'); - class _LoginPageState extends State { final _formKey = GlobalKey(); String _server, _username, _password; @@ -44,8 +42,7 @@ class _LoginPageState extends State { child: TextFormField( onSaved: (serverAddress) => _server = serverAddress, validator: (address) { - var hasMatch = _url.hasMatch(address); - return hasMatch ? null : 'Invalid URL'; + return isUrl(address) ? null : 'Invalid URL'; }, decoration: new InputDecoration( labelText: 'Server Address'), @@ -85,6 +82,19 @@ class _LoginPageState extends State { ? CircularProgressIndicator() : Text('Login'), ))), + Builder( + builder: (context) => ButtonTheme( + height: _loading ? 55.0 : 36.0, + child: RaisedButton( + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + RegisterPage())), + child: _loading + ? CircularProgressIndicator() + : Text('Register'), + ))), ], )), ), @@ -96,7 +106,7 @@ class _LoginPageState extends State { try { var vGlobal = VikunjaGlobal.of(context); var newUser = - await vGlobal.newLoginService(_server).login(_username, _password); + await vGlobal.newUserService(_server).login(_username, _password); vGlobal.changeUser(newUser.user, token: newUser.token, base: _server); } catch (ex) { showDialog( diff --git a/lib/pages/register_page.dart b/lib/pages/register_page.dart new file mode 100644 index 0000000..8a0433a --- /dev/null +++ b/lib/pages/register_page.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:vikunja_app/global.dart'; +import 'package:vikunja_app/utils/validator.dart'; + +class RegisterPage extends StatefulWidget { + @override + _RegisterPageState createState() => _RegisterPageState(); +} + +class _RegisterPageState extends State { + final _formKey = GlobalKey(); + final passwordController = TextEditingController(); + String _server, _username, _email, _password; + bool _loading = false; + + @override + Widget build(BuildContext ctx) { + return Scaffold( + appBar: AppBar( + title: Text('Register to Vikunja'), + ), + body: Builder( + builder: (BuildContext context) => SafeArea( + top: false, + bottom: false, + child: Form( + key: _formKey, + child: ListView( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Image( + image: AssetImage('assets/vikunja_logo.png'), + height: 128.0, + semanticLabel: 'Vikunja Logo', + ), + ), + Padding( + padding: EdgeInsets.all(8.0), + child: TextFormField( + onSaved: (serverAddress) => _server = serverAddress, + validator: (address) { + return isUrl(address) ? null : 'Invalid URL'; + }, + decoration: new InputDecoration( + labelText: 'Server Address'), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + onSaved: (username) => _username = username.trim(), + validator: (username) { + return username.trim().isNotEmpty ? null : 'Please specify a username'; + }, + decoration: + new InputDecoration(labelText: 'Username'), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + onSaved: (email) => _email = email, + validator: (email) { + return isEmail(email) + ? null + : 'Email adress is invalid'; + }, + decoration: + new InputDecoration(labelText: 'Email Address'), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: passwordController, + onSaved: (password) => _password = password, + validator: (password) { + return password.length >= 8 ? null : 'Please use at least 8 characters'; + }, + decoration: + new InputDecoration(labelText: 'Password'), + obscureText: true, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + validator: (password) { + return passwordController.text == password + ? null + : 'Passwords don\'t match.'; + }, + decoration: new InputDecoration( + labelText: 'Repeat Password'), + obscureText: true, + ), + ), + Builder( + builder: (context) => ButtonTheme( + height: _loading ? 55.0 : 36.0, + child: RaisedButton( + onPressed: !_loading + ? () { + if (_formKey.currentState + .validate()) { + Form.of(context).save(); + _registerUser(context); + } else { + print("awhat"); + } + } + : null, + child: _loading + ? CircularProgressIndicator() + : Text('Register'), + ))), + ], + )), + ), + )); + } + + _registerUser(BuildContext context) async { + setState(() => _loading = true); + try { + var vGlobal = VikunjaGlobal.of(context); + var newUserLoggedIn = await vGlobal + .newUserService(_server) + .register(_username, _email, _password); + vGlobal.changeUser(newUserLoggedIn.user, + token: newUserLoggedIn.token, base: _server); + } catch (ex) { + showDialog( + context: context, + builder: (context) => new AlertDialog( + title: const Text( + 'Registration failed! Please check your server url and credentials.'), + actions: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: const Text('CLOSE')) + ], + )); + } finally { + setState(() { + _loading = false; + }); + } + } +} diff --git a/lib/service/mocked_services.dart b/lib/service/mocked_services.dart index 01e914a..df52b6b 100644 --- a/lib/service/mocked_services.dart +++ b/lib/service/mocked_services.dart @@ -151,6 +151,11 @@ class MockedUserService implements UserService { return Future.value(UserTokenPair(_users[1], 'abcdefg')); } + @override + Future register(String username, email, password) { + return Future.value(UserTokenPair(_users[1], 'abcdefg')); + } + @override Future getCurrentUser() { return Future.value(_users[1]); diff --git a/lib/service/services.dart b/lib/service/services.dart index 60363c1..be9f7b8 100644 --- a/lib/service/services.dart +++ b/lib/service/services.dart @@ -29,5 +29,6 @@ abstract class TaskService { abstract class UserService { Future login(String username, password); + Future register(String username, email, password); Future getCurrentUser(); } diff --git a/lib/utils/validator.dart b/lib/utils/validator.dart new file mode 100644 index 0000000..a363bb6 --- /dev/null +++ b/lib/utils/validator.dart @@ -0,0 +1,13 @@ +final RegExp _emailRegex = new RegExp( + r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$'); + +bool isEmail(email) { + return _emailRegex.hasMatch(email); +} + +final RegExp _url = new RegExp( + r'https?:\/\/((([a-zA-Z0-9.\-\_]+)\.[a-zA-Z]+)|(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}))(:[0-9]+)?'); + +bool isUrl(url) { + return _url.hasMatch(url); +}