mirror of
https://github.com/go-vikunja/app
synced 2024-06-01 02:06:51 +00:00
task card formatting
This commit is contained in:
parent
b4989181dc
commit
50eccce18d
|
@ -1,6 +1,9 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/pages/list/task_edit.dart';
|
||||
import 'package:vikunja_app/utils/misc.dart';
|
||||
import 'package:vikunja_app/theme/constants.dart';
|
||||
|
||||
class BucketTaskCard extends StatefulWidget {
|
||||
|
@ -22,40 +25,149 @@ class _BucketTaskCardState extends State<BucketTaskCard> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// default chip height: 32
|
||||
const double chipHeight = 28;
|
||||
final chipConstraints = BoxConstraints(maxHeight: chipHeight);
|
||||
|
||||
final numRow = Row(
|
||||
children: <Widget>[
|
||||
Text('#${_currentTask.id}'),
|
||||
Text(
|
||||
'#${_currentTask.id}',
|
||||
style: TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
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,
|
||||
numRow.children.insert(0, Container(
|
||||
constraints: chipConstraints,
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
child: FittedBox(
|
||||
child: Chip(
|
||||
label: Text('Done'),
|
||||
labelStyle: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).brightness == Brightness.dark
|
||||
? Colors.black : Colors.white,
|
||||
),
|
||||
backgroundColor: vGreen,
|
||||
),
|
||||
),
|
||||
backgroundColor: vGreen,
|
||||
));
|
||||
}
|
||||
|
||||
final titleRow = Row(
|
||||
children: <Widget>[
|
||||
Text(_currentTask.title),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_currentTask.title,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: _currentTask.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
// TODO: add due date
|
||||
final duration = _currentTask.dueDate.difference(DateTime.now());
|
||||
if (_currentTask.dueDate.year > 2) {
|
||||
titleRow.children.add(Container(
|
||||
constraints: chipConstraints,
|
||||
padding: EdgeInsets.only(left: 4),
|
||||
child: FittedBox(
|
||||
child: Chip(
|
||||
avatar: Icon(
|
||||
Icons.calendar_month,
|
||||
color: duration.isNegative ? Colors.red : null,
|
||||
),
|
||||
label: Text(durationToHumanReadable(duration)),
|
||||
labelStyle: duration.isNegative ? TextStyle(color: Colors.red) : null,
|
||||
backgroundColor: duration.isNegative ? Colors.red.withAlpha(20) : null,
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
final labelRow = Row();
|
||||
// TODO: add labels, checklist completion, attachment icon, description icon
|
||||
final labelRow = Wrap(
|
||||
children: <Widget>[],
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
);
|
||||
_currentTask.labels?.sort((a, b) => a.title.compareTo(b.title));
|
||||
_currentTask.labels?.asMap()?.forEach((i, label) {
|
||||
labelRow.children.add(Chip(
|
||||
label: Text(label.title),
|
||||
labelStyle: TextStyle(color: label.textColor),
|
||||
backgroundColor: label.color,
|
||||
));
|
||||
});
|
||||
if (_currentTask.description.isNotEmpty) {
|
||||
final uncompletedTaskCount = '* [ ]'.allMatches(_currentTask.description).length;
|
||||
final completedTaskCount = '* [x]'.allMatches(_currentTask.description).length;
|
||||
final taskCount = uncompletedTaskCount + completedTaskCount;
|
||||
if (taskCount > 0) {
|
||||
labelRow.children.add(Chip(
|
||||
avatar: Container(
|
||||
constraints: BoxConstraints(maxHeight: 16, maxWidth: 16),
|
||||
child: CircularProgressIndicator(
|
||||
value: uncompletedTaskCount == 0
|
||||
? 1 : uncompletedTaskCount.toDouble() / taskCount.toDouble(),
|
||||
backgroundColor: Colors.grey,
|
||||
) ,
|
||||
),
|
||||
label: Text(
|
||||
(uncompletedTaskCount == 0 ? '' : '$completedTaskCount of ')
|
||||
+ '$taskCount tasks'
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
if (_currentTask.attachments != null && _currentTask.attachments.isNotEmpty) {
|
||||
labelRow.children.add(Chip(
|
||||
label: Transform.rotate(
|
||||
angle: -pi / 4.0,
|
||||
child: Icon(Icons.attachment),
|
||||
),
|
||||
));
|
||||
}
|
||||
if (_currentTask.description.isNotEmpty) {
|
||||
labelRow.children.add(Chip(
|
||||
label: Icon(Icons.notes),
|
||||
));
|
||||
}
|
||||
|
||||
final rowConstraints = BoxConstraints(minHeight: chipHeight);
|
||||
return Card(
|
||||
color: _currentTask.color,
|
||||
child: InkWell(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[numRow, titleRow, labelRow],
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
// Remove enforced margins
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
constraints: rowConstraints,
|
||||
child: numRow,
|
||||
),
|
||||
Container(
|
||||
constraints: rowConstraints,
|
||||
child: titleRow,
|
||||
),
|
||||
Padding(
|
||||
padding: labelRow.children.isNotEmpty
|
||||
? EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: labelRow,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
|
|
|
@ -75,7 +75,7 @@ class TaskTileState extends State<TaskTile> {
|
|||
controlAffinity: ListTileControlAffinity.leading,
|
||||
value: _currentTask.done ?? false,
|
||||
subtitle: widget.showInfo && _currentTask.dueDate.year > 2 ?
|
||||
Text("Due in " + durationToHumanReadable(durationUntilDue),style: TextStyle(color: durationUntilDue.isNegative ? Colors.red : null),)
|
||||
Text("Due " + durationToHumanReadable(durationUntilDue), style: TextStyle(color: durationUntilDue.isNegative ? Colors.red : null),)
|
||||
: _currentTask.description == null || _currentTask.description.isEmpty
|
||||
? null
|
||||
: Text(_currentTask.description),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:ui';
|
||||
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
import 'package:vikunja_app/theme/constants.dart';
|
||||
|
||||
class Label {
|
||||
final int id;
|
||||
|
@ -13,7 +14,7 @@ class Label {
|
|||
{this.id,
|
||||
this.title,
|
||||
this.description,
|
||||
this.color,
|
||||
this.color = vLabelDefaultColor,
|
||||
this.created,
|
||||
this.updated,
|
||||
this.createdBy});
|
||||
|
@ -39,4 +40,6 @@ class Label {
|
|||
'updated': updated?.toUtc()?.toIso8601String(),
|
||||
'created': created?.toUtc()?.toIso8601String(),
|
||||
};
|
||||
|
||||
Color get textColor => color.computeLuminance() > 0.5 ? vLabelDark : vLabelLight;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:vikunja_app/components/date_extension.dart';
|
||||
|
||||
import 'package:vikunja_app/models/label.dart';
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
import 'package:vikunja_app/models/taskAttachment.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class Task {
|
||||
|
@ -12,10 +13,12 @@ class Task {
|
|||
List<DateTime> reminderDates;
|
||||
String title, description;
|
||||
bool done;
|
||||
Color color;
|
||||
User createdBy;
|
||||
Duration repeatAfter;
|
||||
List<Task> subtasks;
|
||||
List<Label> labels;
|
||||
List<TaskAttachment> attachments;
|
||||
bool loading = false;
|
||||
// TODO: add kanbanPosition, position(?)
|
||||
|
||||
|
@ -31,8 +34,10 @@ class Task {
|
|||
this.parentTaskId,
|
||||
this.priority,
|
||||
this.repeatAfter,
|
||||
this.color,
|
||||
this.subtasks,
|
||||
this.labels,
|
||||
this.attachments,
|
||||
this.created,
|
||||
this.updated,
|
||||
this.createdBy,
|
||||
|
@ -54,6 +59,9 @@ class Task {
|
|||
parentTaskId = json['parent_task_id'],
|
||||
priority = json['priority'],
|
||||
repeatAfter = Duration(seconds: json['repeat_after']),
|
||||
color = json['hex_color'] == ''
|
||||
? null
|
||||
: new Color(int.parse(json['hex_color'], radix: 16) + 0xFF000000),
|
||||
labels = (json['labels'] as List<dynamic>)
|
||||
?.map((label) => Label.fromJson(label))
|
||||
?.cast<Label>()
|
||||
|
@ -62,6 +70,10 @@ class Task {
|
|||
?.map((subtask) => Task.fromJson(subtask))
|
||||
?.cast<Task>()
|
||||
?.toList(),
|
||||
attachments = (json['attachments'] as List<dynamic>)
|
||||
?.map((attachment) => TaskAttachment.fromJSON(attachment))
|
||||
?.cast<TaskAttachment>()
|
||||
?.toList(),
|
||||
updated = DateTime.parse(json['updated']),
|
||||
created = DateTime.parse(json['created']),
|
||||
listId = json['list_id'],
|
||||
|
@ -82,11 +94,17 @@ class Task {
|
|||
'end_date': endDate?.toUtc()?.toIso8601String(),
|
||||
'priority': priority,
|
||||
'repeat_after': repeatAfter?.inSeconds,
|
||||
'hex_color': color?.value?.toRadixString(16)?.padLeft(8, '0')?.substring(2),
|
||||
'labels': labels?.map((label) => label.toJSON())?.toList(),
|
||||
'subtasks': subtasks?.map((subtask) => subtask.toJSON())?.toList(),
|
||||
'attachments': attachments?.map((attachment) => attachment.toJSON())?.toList(),
|
||||
'bucket_id': bucketId,
|
||||
'created_by': createdBy?.toJSON(),
|
||||
'updated': updated?.toUtc()?.toIso8601String(),
|
||||
'created': created?.toUtc()?.toIso8601String(),
|
||||
};
|
||||
|
||||
Color get textColor => color != null
|
||||
? color.computeLuminance() > 0.5 ? Colors.black : Colors.white
|
||||
: null;
|
||||
}
|
||||
|
|
34
lib/models/taskAttachment.dart
Normal file
34
lib/models/taskAttachment.dart
Normal file
|
@ -0,0 +1,34 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'package:vikunja_app/models/user.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class TaskAttachment {
|
||||
int id, taskId;
|
||||
DateTime created;
|
||||
User createdBy;
|
||||
// TODO: add file
|
||||
|
||||
TaskAttachment({
|
||||
@required this.id,
|
||||
@required this.taskId,
|
||||
this.created,
|
||||
this.createdBy,
|
||||
});
|
||||
|
||||
TaskAttachment.fromJSON(Map<String, dynamic> json)
|
||||
: id = json['id'],
|
||||
taskId = json['task_id'],
|
||||
created = DateTime.parse(json['created']),
|
||||
createdBy = json['created_by'] == null
|
||||
? null
|
||||
: User.fromJson(json['created_by']);
|
||||
|
||||
toJSON() => {
|
||||
'id': id,
|
||||
'task_id': taskId,
|
||||
'created': created?.toUtc()?.toIso8601String(),
|
||||
'created_by': createdBy?.toJSON(),
|
||||
};
|
||||
}
|
|
@ -71,16 +71,28 @@ class _ListPageState extends State<ListPage> {
|
|||
child: taskState.tasks.length > 0 || taskState.buckets.length > 0
|
||||
? ListenableProvider.value(
|
||||
value: taskState,
|
||||
child: () {
|
||||
switch (_viewIndex) {
|
||||
case 0:
|
||||
return _listView(context);
|
||||
case 1:
|
||||
return _kanbanView(context);
|
||||
default:
|
||||
return _listView(context);
|
||||
}
|
||||
}(),
|
||||
child: Theme(
|
||||
data: (ThemeData base) {
|
||||
return base.copyWith(
|
||||
chipTheme: base.chipTheme.copyWith(
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 2),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}(Theme.of(context)),
|
||||
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,
|
||||
|
@ -195,14 +207,22 @@ class _ListPageState extends State<ListPage> {
|
|||
}
|
||||
|
||||
Container _buildBucketTile([Bucket bucket]) {
|
||||
final deviceData = MediaQuery.of(context);
|
||||
return Container(
|
||||
width: MediaQuery.of(context).size.width * 0.8,
|
||||
width: deviceData.size.width
|
||||
* (deviceData.orientation == Orientation.portrait ? 0.8 : 0.4),
|
||||
child: Column(
|
||||
children: () {
|
||||
if (bucket != null) {
|
||||
return <Widget>[
|
||||
ListTile(
|
||||
title: Text(bucket.title),
|
||||
title: Text(
|
||||
bucket.title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
trailing: Icon(Icons.more_vert),
|
||||
),
|
||||
Expanded(
|
||||
|
@ -218,6 +238,7 @@ class _ListPageState extends State<ListPage> {
|
|||
title: TextButton.icon(
|
||||
onPressed: () => _addBucketDialog(context),
|
||||
label: Text('Create Bucket'),
|
||||
style: ButtonStyle(alignment: Alignment.centerLeft),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
|
@ -287,7 +308,7 @@ class _ListPageState extends State<ListPage> {
|
|||
builder: (_) => AddDialog(
|
||||
onAdd: (title) => _addItem(title, context, bucket),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Task Name',
|
||||
labelText: (bucket != null ? '${bucket.title}: ' : '') + 'New Task Name',
|
||||
hintText: 'eg. Milk',
|
||||
),
|
||||
),
|
||||
|
|
|
@ -21,7 +21,7 @@ const vButtonShadow = Color(0xFFb2d9ff);
|
|||
|
||||
const vLabelLight = Color(0xFFf2f2f2);
|
||||
const vLabelDark = Color(0xFF4a4a4a);
|
||||
const vLabelDefaultColor = vGreen;
|
||||
const vLabelDefaultColor = Color(0xFFe8e8e8);
|
||||
|
||||
///////////
|
||||
// Paddings
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
String durationToHumanReadable(Duration dur) {
|
||||
var durString = '';
|
||||
if(dur.inDays.abs() > 1)
|
||||
return dur.inDays.toString() + " days";
|
||||
if(dur.inDays.abs() == 1)
|
||||
return dur.inDays.toString() + " day";
|
||||
durString = dur.inDays.abs().toString() + " days";
|
||||
else if(dur.inDays.abs() == 1)
|
||||
durString = dur.inDays.abs().toString() + " day";
|
||||
|
||||
if(dur.inHours.abs() > 1)
|
||||
return dur.inHours.toString() + " hours";
|
||||
if(dur.inHours.abs() == 1)
|
||||
return dur.inHours.toString() + " hour";
|
||||
else if(dur.inHours.abs() > 1)
|
||||
durString = dur.inHours.abs().toString() + " hours";
|
||||
else if(dur.inHours.abs() == 1)
|
||||
durString = dur.inHours.abs().toString() + " hour";
|
||||
|
||||
if(dur.inMinutes.abs() > 1)
|
||||
return dur.inMinutes.toString() + " minutes";
|
||||
if(dur.inMinutes.abs() == 1)
|
||||
return dur.inMinutes.toString() + " minute";
|
||||
return "under 1 minute";
|
||||
else if(dur.inMinutes.abs() > 1)
|
||||
durString = dur.inMinutes.abs().toString() + " minutes";
|
||||
else if(dur.inMinutes.abs() == 1)
|
||||
durString = dur.inMinutes.abs().toString() + " minute";
|
||||
else durString = "less than a minute";
|
||||
|
||||
if (dur.isNegative) return durString + " ago";
|
||||
return "in " + durString;
|
||||
}
|
Loading…
Reference in New Issue
Block a user