1
0
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:
Paul Nettleton 2022-07-23 18:08:59 -05:00
parent b4989181dc
commit 50eccce18d
8 changed files with 238 additions and 46 deletions

View File

@ -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,

View File

@ -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),

View File

@ -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;
}

View File

@ -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;
}

View 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(),
};
}

View File

@ -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',
),
),

View File

@ -21,7 +21,7 @@ const vButtonShadow = Color(0xFFb2d9ff);
const vLabelLight = Color(0xFFf2f2f2);
const vLabelDark = Color(0xFF4a4a4a);
const vLabelDefaultColor = vGreen;
const vLabelDefaultColor = Color(0xFFe8e8e8);
///////////
// Paddings

View File

@ -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;
}