Merge branch 'master' of ssh://git.kolaente.de:9022/vikunja/app into feature/drone-ios
All checks were successful
the build was successful
All checks were successful
the build was successful
This commit is contained in:
commit
fcf0d9ecf9
|
@ -34,7 +34,7 @@ pipeline:
|
||||||
- flutter packages get
|
- flutter packages get
|
||||||
- make build-all
|
- make build-all
|
||||||
- mkdir apks
|
- mkdir apks
|
||||||
- mv build/app/outputs/apk/*/*.apk apks
|
- mv build/app/outputs/apk/*/*/*.apk apks
|
||||||
when:
|
when:
|
||||||
event: [ push, tag ]
|
event: [ push, tag ]
|
||||||
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="ProjectRunConfigurationManager">
|
|
||||||
<configuration default="false" name="main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
|
|
||||||
<option name="filePath" value="$PROJECT_DIR$/lib/main.dart" />
|
|
||||||
<method />
|
|
||||||
</configuration>
|
|
||||||
</component>
|
|
6
Makefile
6
Makefile
|
@ -19,15 +19,15 @@ build-all: build-release build-debug build-profile
|
||||||
|
|
||||||
.PHONY: build-release
|
.PHONY: build-release
|
||||||
build-release:
|
build-release:
|
||||||
flutter build apk --release --build-name=$(VERSION)
|
flutter build apk --release --build-name=$(VERSION) --flavor main
|
||||||
|
|
||||||
.PHONY: build-debug
|
.PHONY: build-debug
|
||||||
build-debug:
|
build-debug:
|
||||||
flutter build apk --debug --build-name=$(VERSION)
|
flutter build apk --debug --build-name=$(VERSION) --flavor unsigned
|
||||||
|
|
||||||
.PHONY: build-profile
|
.PHONY: build-profile
|
||||||
build-profile:
|
build-profile:
|
||||||
flutter build apk --profile --build-name=$(VERSION)
|
flutter build apk --profile --build-name=$(VERSION) --flavor unsigned
|
||||||
|
|
||||||
.PHONY: build-ios-all
|
.PHONY: build-ios-all
|
||||||
build-ios-all: build-ios-release build-ios-debug build-ios-profile
|
build-ios-all: build-ios-release build-ios-debug build-ios-profile
|
||||||
|
|
|
@ -38,12 +38,38 @@ android {
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
// 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
|
minSdkVersion 18
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
flavorDimensions "deploy"
|
||||||
|
|
||||||
|
productFlavors {
|
||||||
|
fdroid {
|
||||||
|
dimension "deploy"
|
||||||
|
signingConfig null
|
||||||
|
}
|
||||||
|
unsigned {
|
||||||
|
dimension "deploy"
|
||||||
|
signingConfig null
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
dimension "deploy"
|
||||||
|
signingConfig signingConfigs.debug // TODO add signing key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android.applicationVariants.all { variant ->
|
||||||
|
if (variant.flavorName == "fdroid") {
|
||||||
|
variant.outputs.all { output ->
|
||||||
|
output.outputFileName = "app-fdroid-release.apk"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|
|
@ -19,6 +19,16 @@ class UserAPIService extends APIService implements UserService {
|
||||||
.then((user) => UserTokenPair(user, token));
|
.then((user) => UserTokenPair(user, token));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserTokenPair> 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
|
@override
|
||||||
Future<User> getCurrentUser() {
|
Future<User> getCurrentUser() {
|
||||||
return client.get('/user').then((map) => User.fromJson(map));
|
return client.get('/user').then((map) => User.fromJson(map));
|
||||||
|
|
39
lib/components/AddDialog.dart
Normal file
39
lib/components/AddDialog.dart
Normal 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);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
89
lib/components/TaskTile.dart
Normal file
89
lib/components/TaskTile.dart
Normal 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);
|
|
@ -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);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,8 +37,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
||||||
Client get client => _client;
|
Client get client => _client;
|
||||||
|
|
||||||
UserManager get userManager => new UserManager(_storage);
|
UserManager get userManager => new UserManager(_storage);
|
||||||
UserService get userService => new UserAPIService(_client);
|
UserService newUserService(base) => new UserAPIService(Client(null, base));
|
||||||
UserService newLoginService(base) => new UserAPIService(Client(null, base));
|
|
||||||
NamespaceService get namespaceService => new NamespaceAPIService(client);
|
NamespaceService get namespaceService => new NamespaceAPIService(client);
|
||||||
TaskService get taskService => new TaskAPIService(client);
|
TaskService get taskService => new TaskAPIService(client);
|
||||||
ListService get listService => new ListAPIService(client);
|
ListService get listService => new ListAPIService(client);
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -102,7 +82,7 @@ class HomePageState extends State<HomePage> {
|
||||||
)));
|
)));
|
||||||
|
|
||||||
return new Scaffold(
|
return new Scaffold(
|
||||||
appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vakunja')),
|
appBar: AppBar(title: new Text(_currentNamespace?.name ?? 'Vikunja')),
|
||||||
drawer: new Drawer(
|
drawer: new Drawer(
|
||||||
child: new Column(children: <Widget>[
|
child: new Column(children: <Widget>[
|
||||||
new UserAccountsDrawerHeader(
|
new UserAccountsDrawerHeader(
|
||||||
|
@ -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(
|
||||||
|
|
|
@ -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))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:vikunja_app/global.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 {
|
class LoginPage extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
_LoginPageState createState() => _LoginPageState();
|
_LoginPageState createState() => _LoginPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
final RegExp _url = new RegExp(
|
|
||||||
r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)');
|
|
||||||
|
|
||||||
class _LoginPageState extends State<LoginPage> {
|
class _LoginPageState extends State<LoginPage> {
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
String _server, _username, _password;
|
String _server, _username, _password;
|
||||||
|
@ -44,8 +42,7 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
onSaved: (serverAddress) => _server = serverAddress,
|
onSaved: (serverAddress) => _server = serverAddress,
|
||||||
validator: (address) {
|
validator: (address) {
|
||||||
var hasMatch = _url.hasMatch(address);
|
return isUrl(address) ? null : 'Invalid URL';
|
||||||
return hasMatch ? null : 'Invalid URL';
|
|
||||||
},
|
},
|
||||||
decoration: new InputDecoration(
|
decoration: new InputDecoration(
|
||||||
labelText: 'Server Address'),
|
labelText: 'Server Address'),
|
||||||
|
@ -85,6 +82,19 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
? CircularProgressIndicator()
|
? CircularProgressIndicator()
|
||||||
: Text('Login'),
|
: 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,10 +106,9 @@ class _LoginPageState extends State<LoginPage> {
|
||||||
try {
|
try {
|
||||||
var vGlobal = VikunjaGlobal.of(context);
|
var vGlobal = VikunjaGlobal.of(context);
|
||||||
var newUser =
|
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);
|
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(
|
||||||
|
|
152
lib/pages/register_page.dart
Normal file
152
lib/pages/register_page.dart
Normal file
|
@ -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<RegisterPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
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: <Widget>[
|
||||||
|
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: <Widget>[
|
||||||
|
FlatButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('CLOSE'))
|
||||||
|
],
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -151,6 +151,11 @@ class MockedUserService implements UserService {
|
||||||
return Future.value(UserTokenPair(_users[1], 'abcdefg'));
|
return Future.value(UserTokenPair(_users[1], 'abcdefg'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UserTokenPair> register(String username, email, password) {
|
||||||
|
return Future.value(UserTokenPair(_users[1], 'abcdefg'));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<User> getCurrentUser() {
|
Future<User> getCurrentUser() {
|
||||||
return Future.value(_users[1]);
|
return Future.value(_users[1]);
|
||||||
|
|
|
@ -29,5 +29,6 @@ abstract class TaskService {
|
||||||
|
|
||||||
abstract class UserService {
|
abstract class UserService {
|
||||||
Future<UserTokenPair> login(String username, password);
|
Future<UserTokenPair> login(String username, password);
|
||||||
|
Future<UserTokenPair> register(String username, email, password);
|
||||||
Future<User> getCurrentUser();
|
Future<User> getCurrentUser();
|
||||||
}
|
}
|
||||||
|
|
13
lib/utils/validator.dart
Normal file
13
lib/utils/validator.dart
Normal file
|
@ -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);
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
name: vikunja_app
|
name: vikunja_app
|
||||||
description: Vikunja as Flutter cross platform app
|
description: Vikunja as Flutter cross platform app
|
||||||
|
|
||||||
version: 1.0.0+1
|
version: 0.0.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.0.0-dev.63.0 <3.0.0"
|
sdk: ">=2.0.0-dev.63.0 <3.0.0"
|
||||||
|
|
Reference in New Issue
Block a user