mirror of
https://github.com/go-vikunja/app
synced 2024-06-01 02:06:51 +00:00
basic kanban view
This commit is contained in:
parent
4a4ad2c9b3
commit
ad665e68cc
53
lib/api/bucket_implementation.dart
Normal file
53
lib/api/bucket_implementation.dart
Normal file
|
@ -0,0 +1,53 @@
|
|||
import 'package:vikunja_app/api/client.dart';
|
||||
import 'package:vikunja_app/api/response.dart';
|
||||
import 'package:vikunja_app/api/service.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
import 'package:vikunja_app/service/services.dart';
|
||||
|
||||
class BucketAPIService extends APIService implements BucketService {
|
||||
BucketAPIService(Client client) : super(client);
|
||||
|
||||
@override
|
||||
Future<Bucket> add(int listId, Bucket bucket) {
|
||||
return client
|
||||
.put('/lists/$listId/buckets', body: bucket.toJSON())
|
||||
.then((response) => Bucket.fromJSON(response.body));
|
||||
}
|
||||
|
||||
@override
|
||||
Future delete(int listId, int bucketId) {
|
||||
return client
|
||||
.delete('/lists/$listId/buckets/$bucketId');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Bucket> get(int listId, int bucketId) {
|
||||
// Might not exist in the API, it isn't in the docs
|
||||
return client
|
||||
.get('/lists/$listId/buckets/$bucketId')
|
||||
.then((response) => Bucket.fromJSON(response.body));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Response> getAllByList(int listId,
|
||||
[Map<String, List<String>> queryParameters]) {
|
||||
return client
|
||||
.get('/lists/$listId/buckets', queryParameters)
|
||||
.then((response) => new Response(
|
||||
convertList(response.body, (result) => Bucket.fromJSON(result)),
|
||||
response.statusCode,
|
||||
response.headers
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
// TODO: implement maxPages
|
||||
int get maxPages => maxPages;
|
||||
|
||||
@override
|
||||
Future<Bucket> update(Bucket bucket) {
|
||||
return client
|
||||
.post('/lists/${bucket.listId}/buckets/${bucket.id}', body: bucket.toJSON())
|
||||
.then((response) => Bucket.fromJSON(response.body));
|
||||
}
|
||||
}
|
54
lib/components/BucketListView.dart
Normal file
54
lib/components/BucketListView.dart
Normal file
|
@ -0,0 +1,54 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/components/BucketTaskCard.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
|
||||
class BucketListView extends StatefulWidget {
|
||||
final Bucket bucket;
|
||||
final Function onEdit;
|
||||
final Function onAddTask;
|
||||
|
||||
const BucketListView({Key key, @required this.bucket, this.onEdit, this.onAddTask})
|
||||
: assert(bucket != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<BucketListView> createState() => _BucketListViewState(this.bucket);
|
||||
}
|
||||
|
||||
class _BucketListViewState extends State<BucketListView> {
|
||||
Bucket _currentBucket;
|
||||
|
||||
_BucketListViewState(this._currentBucket)
|
||||
: assert(_currentBucket != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10),
|
||||
itemBuilder: (context, i) {
|
||||
if (i == 0) {
|
||||
return Text(_currentBucket.title);
|
||||
}
|
||||
|
||||
final index = i - 1;
|
||||
|
||||
if (_currentBucket.tasks == null || index >= _currentBucket.tasks.length)
|
||||
return null;
|
||||
|
||||
return index < _currentBucket.tasks.length
|
||||
? _buildBucketTaskTile(_currentBucket.tasks[index])
|
||||
: null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
BucketTaskCard _buildBucketTaskTile(Task task) {
|
||||
return BucketTaskCard(
|
||||
task: task,
|
||||
);
|
||||
}
|
||||
}
|
60
lib/components/BucketTaskCard.dart
Normal file
60
lib/components/BucketTaskCard.dart
Normal file
|
@ -0,0 +1,60 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/theme/constants.dart';
|
||||
|
||||
class BucketTaskCard extends StatefulWidget {
|
||||
final Task task;
|
||||
|
||||
const BucketTaskCard({Key key, @required this.task})
|
||||
: assert(task != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<BucketTaskCard> createState() => _BucketTaskCardState(this.task);
|
||||
}
|
||||
|
||||
class _BucketTaskCardState extends State<BucketTaskCard> {
|
||||
Task _currentTask;
|
||||
|
||||
_BucketTaskCardState(this._currentTask)
|
||||
: assert(_currentTask != null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final numRow = Row(
|
||||
children: <Widget>[
|
||||
Text('#${_currentTask.id}'),
|
||||
],
|
||||
);
|
||||
if (_currentTask.done) {
|
||||
numRow.children.insert(0, Chip(
|
||||
label: Text('Done'),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme
|
||||
.of(context)
|
||||
.brightness == Brightness.dark
|
||||
? Colors.black : Colors.white,
|
||||
),
|
||||
backgroundColor: vGreen,
|
||||
));
|
||||
}
|
||||
|
||||
final titleRow = Row(
|
||||
children: <Widget>[
|
||||
Text(_currentTask.title),
|
||||
],
|
||||
);
|
||||
// TODO: add due date
|
||||
|
||||
final labelRow = Row();
|
||||
// TODO: add labels, checklist completion, attachment icon, description icon
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[numRow, titleRow, labelRow],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import 'dart:developer' as dev;
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_native_timezone/flutter_native_timezone.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:vikunja_app/api/bucket_implementation.dart';
|
||||
import 'package:vikunja_app/api/client.dart';
|
||||
import 'package:vikunja_app/api/label_task.dart';
|
||||
import 'package:vikunja_app/api/label_task_bulk.dart';
|
||||
|
@ -65,6 +66,8 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
|
|||
|
||||
TaskService get taskService => new TaskAPIService(client);
|
||||
|
||||
BucketService get bucketService => new BucketAPIService(client);
|
||||
|
||||
ListService get listService => new ListAPIService(client, _storage);
|
||||
|
||||
notifs.FlutterLocalNotificationsPlugin get notificationsPlugin => new notifs.FlutterLocalNotificationsPlugin();
|
||||
|
|
61
lib/models/bucket.dart
Normal file
61
lib/models/bucket.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Bucket {
|
||||
int id, listId, limit;
|
||||
double position;
|
||||
String title;
|
||||
DateTime created, updated;
|
||||
User createdBy;
|
||||
bool isDoneBucket;
|
||||
List<Task> tasks;
|
||||
|
||||
Bucket({
|
||||
@required this.id,
|
||||
@required this.listId,
|
||||
this.title,
|
||||
this.position,
|
||||
this.limit,
|
||||
this.isDoneBucket,
|
||||
this.created,
|
||||
this.updated,
|
||||
this.createdBy,
|
||||
this.tasks,
|
||||
});
|
||||
|
||||
Bucket.fromJSON(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
listId = json['list_id'],
|
||||
title = json['title'],
|
||||
position = json['position'] is int
|
||||
? json['position'].toDouble()
|
||||
: json['position'],
|
||||
limit = json['limit'],
|
||||
isDoneBucket = json['is_done_bucket'],
|
||||
created = DateTime.parse(json['created']),
|
||||
updated = DateTime.parse(json['updated']),
|
||||
createdBy = json['created_by'] == null
|
||||
? null
|
||||
: User.fromJson(json['created_by']),
|
||||
tasks = (json['tasks'] as List<dynamic>)
|
||||
?.map((task) => Task.fromJson(task))
|
||||
?.cast<Task>()
|
||||
?.toList();
|
||||
|
||||
toJSON() => {
|
||||
'id': id,
|
||||
'list_id': listId,
|
||||
'title': title,
|
||||
'position': position,
|
||||
'limit': limit,
|
||||
'is_done_bucket': isDoneBucket ?? false,
|
||||
'created': created?.toUtc()?.toIso8601String(),
|
||||
'updated': updated?.toUtc()?.toIso8601String(),
|
||||
'createdBy': createdBy?.toJSON(),
|
||||
'tasks': tasks?.map((task) => task.toJSON())?.toList(),
|
||||
};
|
||||
}
|
|
@ -5,9 +5,11 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
import 'package:vikunja_app/components/AddDialog.dart';
|
||||
import 'package:vikunja_app/components/TaskTile.dart';
|
||||
import 'package:vikunja_app/components/BucketListView.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
import 'package:vikunja_app/pages/list/list_edit.dart';
|
||||
import 'package:vikunja_app/pages/list/task_edit.dart';
|
||||
import 'package:vikunja_app/stores/list_store.dart';
|
||||
|
@ -66,35 +68,19 @@ class _ListPageState extends State<ListPage> {
|
|||
// TODO: it brakes the flow with _loadingTasks and conflicts with the provider
|
||||
body: !taskState.isLoading
|
||||
? RefreshIndicator(
|
||||
child: taskState.tasks.length > 0
|
||||
child: taskState.tasks.length > 0 || taskState.buckets.length > 0
|
||||
? ListenableProvider.value(
|
||||
value: taskState,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i.isOdd) return Divider();
|
||||
|
||||
if (_loadingTasks.isNotEmpty) {
|
||||
final loadingTask = _loadingTasks.removeLast();
|
||||
return _buildLoadingTile(loadingTask);
|
||||
}
|
||||
|
||||
final index = i ~/ 2;
|
||||
|
||||
// This handles the case if there are no more elements in the list left which can be provided by the api
|
||||
if (taskState.maxPages == _currentPage &&
|
||||
index == taskState.tasks.length)
|
||||
return null;
|
||||
|
||||
if (index >= taskState.tasks.length &&
|
||||
_currentPage < taskState.maxPages) {
|
||||
_currentPage++;
|
||||
_loadTasksForPage(_currentPage);
|
||||
}
|
||||
return index < taskState.tasks.length
|
||||
? _buildTile(taskState.tasks[index])
|
||||
: null;
|
||||
}),
|
||||
child: () {
|
||||
switch (_viewIndex) {
|
||||
case 0:
|
||||
return _listView(context);
|
||||
case 1:
|
||||
return _kanbanView(context);
|
||||
default:
|
||||
return _listView(context);
|
||||
}
|
||||
}(),
|
||||
)
|
||||
: Center(child: Text('This list is empty.')),
|
||||
onRefresh: _loadList,
|
||||
|
@ -124,11 +110,63 @@ class _ListPageState extends State<ListPage> {
|
|||
}
|
||||
|
||||
void _onViewTapped(int index) {
|
||||
setState(() {
|
||||
_viewIndex = index;
|
||||
_loadList().then((_) {
|
||||
_currentPage = 1;
|
||||
setState(() {
|
||||
_viewIndex = index;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ListView _listView(BuildContext context) {
|
||||
return ListView.builder(
|
||||
padding: EdgeInsets.symmetric(vertical: 8.0),
|
||||
itemBuilder: (context, i) {
|
||||
if (i.isOdd) return Divider();
|
||||
|
||||
if (_loadingTasks.isNotEmpty) {
|
||||
final loadingTask = _loadingTasks.removeLast();
|
||||
return _buildLoadingTile(loadingTask);
|
||||
}
|
||||
|
||||
final index = i ~/ 2;
|
||||
|
||||
// This handles the case if there are no more elements in the list left which can be provided by the api
|
||||
if (taskState.maxPages == _currentPage &&
|
||||
index == taskState.tasks.length)
|
||||
return null;
|
||||
|
||||
if (index >= taskState.tasks.length &&
|
||||
_currentPage < taskState.maxPages) {
|
||||
_currentPage++;
|
||||
_loadTasksForPage(_currentPage);
|
||||
}
|
||||
return index < taskState.tasks.length
|
||||
? _buildTile(taskState.tasks[index])
|
||||
: null;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ListView _kanbanView(BuildContext context) {
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: EdgeInsets.symmetric(vertical: 10),
|
||||
itemBuilder: (context, i) {
|
||||
if (taskState.maxPages == _currentPage && i == taskState.buckets.length)
|
||||
return null;
|
||||
|
||||
if (i >= taskState.buckets.length && _currentPage < taskState.maxPages) {
|
||||
_currentPage++;
|
||||
_loadBucketsForPage(_currentPage);
|
||||
}
|
||||
return i < taskState.buckets.length
|
||||
? _buildBucketTile(taskState.buckets[i])
|
||||
: null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
TaskTile _buildTile(Task task) {
|
||||
return TaskTile(
|
||||
task: task,
|
||||
|
@ -154,6 +192,12 @@ class _ListPageState extends State<ListPage> {
|
|||
);
|
||||
}
|
||||
|
||||
BucketListView _buildBucketTile(Bucket bucket) {
|
||||
return BucketListView(
|
||||
bucket: bucket,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDisplayDoneTasks() {
|
||||
return VikunjaGlobal.of(context).listService.getDisplayDoneTasks(_list.id)
|
||||
.then((value) {displayDoneTasks = value == "1";});
|
||||
|
@ -175,7 +219,18 @@ class _ListPageState extends State<ListPage> {
|
|||
}
|
||||
|
||||
Future<void> _loadList() async {
|
||||
updateDisplayDoneTasks().then((value) => _loadTasksForPage(1));
|
||||
updateDisplayDoneTasks().then((value) {
|
||||
switch (_viewIndex) {
|
||||
case 0:
|
||||
_loadTasksForPage(1);
|
||||
break;
|
||||
case 1:
|
||||
_loadBucketsForPage(1);
|
||||
break;
|
||||
default:
|
||||
_loadTasksForPage(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _loadTasksForPage(int page) {
|
||||
|
@ -187,6 +242,14 @@ class _ListPageState extends State<ListPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _loadBucketsForPage(int page) {
|
||||
Provider.of<ListProvider>(context, listen: false).loadBuckets(
|
||||
context: context,
|
||||
listId: _list.id,
|
||||
page: page
|
||||
);
|
||||
}
|
||||
|
||||
_addItemDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:vikunja_app/models/list.dart';
|
|||
import 'package:vikunja_app/models/namespace.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
|
||||
import '../models/server.dart';
|
||||
|
||||
|
@ -112,6 +113,17 @@ abstract class TaskService {
|
|||
int get maxPages;
|
||||
}
|
||||
|
||||
abstract class BucketService {
|
||||
Future<Bucket> get(int listId, int bucketId);
|
||||
Future<Bucket> update(Bucket bucket);
|
||||
Future delete(int listId, int bucketId);
|
||||
Future<Bucket> add(int listId, Bucket bucket);
|
||||
Future<Response> getAllByList(int listId,
|
||||
[Map<String, List<String>> queryParameters]);
|
||||
|
||||
int get maxPages;
|
||||
}
|
||||
|
||||
abstract class UserService {
|
||||
Future<UserTokenPair> login(String username, password, {bool rememberMe = false, String totp});
|
||||
Future<UserTokenPair> register(String username, email, password);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
|
||||
class ListProvider with ChangeNotifier {
|
||||
|
@ -8,6 +9,7 @@ class ListProvider with ChangeNotifier {
|
|||
|
||||
// TODO: Streams
|
||||
List<Task> _tasks = [];
|
||||
List<Bucket> _buckets = [];
|
||||
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
|
@ -20,6 +22,13 @@ class ListProvider with ChangeNotifier {
|
|||
|
||||
List<Task> get tasks => _tasks;
|
||||
|
||||
set buckets(List<Bucket> buckets) {
|
||||
_buckets = buckets;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Bucket> get buckets => _buckets;
|
||||
|
||||
void loadTasks({BuildContext context, int listId, int page = 1, bool displayDoneTasks = true}) {
|
||||
_tasks = [];
|
||||
_isLoading = true;
|
||||
|
@ -48,6 +57,26 @@ class ListProvider with ChangeNotifier {
|
|||
});
|
||||
}
|
||||
|
||||
void loadBuckets({BuildContext context, int listId, int page = 1}) {
|
||||
_buckets = [];
|
||||
_isLoading = true;
|
||||
notifyListeners();
|
||||
|
||||
Map<String, List<String>> queryParams = {
|
||||
"page": [page.toString()]
|
||||
};
|
||||
|
||||
VikunjaGlobal.of(context).bucketService.getAllByList(listId, queryParams).then((response) {
|
||||
if (response.headers["x-pagination-total-pages"] != null) {
|
||||
_maxPages = int.parse(response.headers["x-pagination-total-pages"]);
|
||||
}
|
||||
_buckets.addAll(response.body);
|
||||
|
||||
_isLoading = false;
|
||||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> addTaskByTitle(
|
||||
{BuildContext context, String title, int listId}) {
|
||||
var globalState = VikunjaGlobal.of(context);
|
||||
|
|
|
@ -38,9 +38,8 @@ ThemeData _buildVikunjaTheme(ThemeData base) {
|
|||
// Make bottomNavigationBar backgroundColor darker to provide more separation
|
||||
backgroundColor: () {
|
||||
final _hslColor = HSLColor.fromColor(
|
||||
base.bottomNavigationBarTheme.backgroundColor != null
|
||||
? base.bottomNavigationBarTheme.backgroundColor
|
||||
: base.scaffoldBackgroundColor
|
||||
base.bottomNavigationBarTheme.backgroundColor
|
||||
?? base.scaffoldBackgroundColor
|
||||
);
|
||||
return _hslColor.withLightness(max(_hslColor.lightness - 0.03, 0)).toColor();
|
||||
}(),
|
||||
|
|
Loading…
Reference in New Issue
Block a user