mirror of
https://github.com/go-vikunja/app
synced 2024-05-31 17:57:14 +00:00
move tasks around buckets, some cleanup
This commit is contained in:
parent
0762a7ae14
commit
1315d7812c
|
@ -1,47 +1,74 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/pages/list/task_edit.dart';
|
||||
import 'package:vikunja_app/stores/list_store.dart';
|
||||
import 'package:vikunja_app/utils/misc.dart';
|
||||
import 'package:vikunja_app/theme/constants.dart';
|
||||
|
||||
enum DropLocation {above, below, none}
|
||||
|
||||
class TaskData {
|
||||
final Task task;
|
||||
final Size size;
|
||||
TaskData(this.task, this.size);
|
||||
}
|
||||
|
||||
class BucketTaskCard extends StatefulWidget {
|
||||
final Task task;
|
||||
final int index;
|
||||
final DragUpdateCallback onDragUpdate;
|
||||
final void Function(Task, int) onAccept;
|
||||
|
||||
const BucketTaskCard({Key key, @required this.task})
|
||||
: assert(task != null),
|
||||
super(key: key);
|
||||
const BucketTaskCard({
|
||||
Key key,
|
||||
@required this.task,
|
||||
@required this.index,
|
||||
@required this.onDragUpdate,
|
||||
@required this.onAccept,
|
||||
}) : assert(task != null),
|
||||
assert(index != null),
|
||||
assert(onDragUpdate != null),
|
||||
assert(onAccept != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
State<BucketTaskCard> createState() => _BucketTaskCardState(this.task);
|
||||
State<BucketTaskCard> createState() => _BucketTaskCardState();
|
||||
}
|
||||
|
||||
class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAliveClientMixin {
|
||||
Task _currentTask;
|
||||
|
||||
_BucketTaskCardState(this._currentTask)
|
||||
: assert(_currentTask != null);
|
||||
Size _cardSize;
|
||||
bool _dragging = false;
|
||||
DropLocation _dropLocation = DropLocation.none;
|
||||
TaskData _dropData;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
if (_cardSize == null) _updateCardSize(context);
|
||||
|
||||
final taskState = Provider.of<ListProvider>(context);
|
||||
final bucket = taskState.buckets[taskState.buckets.indexWhere((b) => b.id == widget.task.bucketId)];
|
||||
// default chip height: 32
|
||||
const double chipHeight = 28;
|
||||
final chipConstraints = BoxConstraints(maxHeight: chipHeight);
|
||||
const chipConstraints = BoxConstraints(maxHeight: chipHeight);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final numRow = Row(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'#${_currentTask.id}',
|
||||
'#${widget.task.id}',
|
||||
style: theme.textTheme.subtitle2.copyWith(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
if (_currentTask.done) {
|
||||
if (widget.task.done) {
|
||||
numRow.children.insert(0, Container(
|
||||
constraints: chipConstraints,
|
||||
padding: EdgeInsets.only(right: 4),
|
||||
|
@ -63,16 +90,16 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
_currentTask.title,
|
||||
widget.task.title,
|
||||
style: theme.textTheme.titleMedium.copyWith(
|
||||
color: _currentTask.textColor,
|
||||
color: widget.task.textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
final duration = _currentTask.dueDate.difference(DateTime.now());
|
||||
if (_currentTask.dueDate.year > 2) {
|
||||
final duration = widget.task.dueDate.difference(DateTime.now());
|
||||
if (widget.task.dueDate.year > 2) {
|
||||
titleRow.children.add(Container(
|
||||
constraints: chipConstraints,
|
||||
padding: EdgeInsets.only(left: 4),
|
||||
|
@ -97,8 +124,8 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
);
|
||||
_currentTask.labels?.sort((a, b) => a.title.compareTo(b.title));
|
||||
_currentTask.labels?.asMap()?.forEach((i, label) {
|
||||
widget.task.labels?.sort((a, b) => a.title.compareTo(b.title));
|
||||
widget.task.labels?.asMap()?.forEach((i, label) {
|
||||
labelRow.children.add(Chip(
|
||||
label: Text(label.title),
|
||||
labelStyle: theme.textTheme.labelLarge.copyWith(
|
||||
|
@ -107,9 +134,9 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
backgroundColor: label.color,
|
||||
));
|
||||
});
|
||||
if (_currentTask.description.isNotEmpty) {
|
||||
final uncompletedTaskCount = '* [ ]'.allMatches(_currentTask.description).length;
|
||||
final completedTaskCount = '* [x]'.allMatches(_currentTask.description).length;
|
||||
if (widget.task.description.isNotEmpty) {
|
||||
final uncompletedTaskCount = '* [ ]'.allMatches(widget.task.description).length;
|
||||
final completedTaskCount = '* [x]'.allMatches(widget.task.description).length;
|
||||
final taskCount = uncompletedTaskCount + completedTaskCount;
|
||||
if (taskCount > 0) {
|
||||
final iconSize = (theme.textTheme.labelLarge.fontSize ?? 14) + 2;
|
||||
|
@ -129,7 +156,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
));
|
||||
}
|
||||
}
|
||||
if (_currentTask.attachments != null && _currentTask.attachments.isNotEmpty) {
|
||||
if (widget.task.attachments != null && widget.task.attachments.isNotEmpty) {
|
||||
labelRow.children.add(Chip(
|
||||
label: Transform.rotate(
|
||||
angle: -pi / 4.0,
|
||||
|
@ -137,15 +164,15 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
),
|
||||
));
|
||||
}
|
||||
if (_currentTask.description.isNotEmpty) {
|
||||
if (widget.task.description.isNotEmpty) {
|
||||
labelRow.children.add(Chip(
|
||||
label: Icon(Icons.notes),
|
||||
));
|
||||
}
|
||||
|
||||
final rowConstraints = BoxConstraints(minHeight: chipHeight);
|
||||
return Card(
|
||||
color: _currentTask.color,
|
||||
final card = Card(
|
||||
color: widget.task.color,
|
||||
child: InkWell(
|
||||
child: Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
|
@ -153,7 +180,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(4),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
@ -168,7 +195,7 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
),
|
||||
Padding(
|
||||
padding: labelRow.children.isNotEmpty
|
||||
? EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
? const EdgeInsets.only(top: 8) : EdgeInsets.zero,
|
||||
child: labelRow,
|
||||
),
|
||||
],
|
||||
|
@ -179,17 +206,122 @@ class _BucketTaskCardState extends State<BucketTaskCard> with AutomaticKeepAlive
|
|||
FocusScope.of(context).unfocus();
|
||||
Navigator.push<Task>(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => TaskEditPage(
|
||||
task: _currentTask,
|
||||
)),
|
||||
).then((task) => setState(() {
|
||||
if (task != null) _currentTask = task;
|
||||
}));
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TaskEditPage(
|
||||
task: widget.task,
|
||||
taskState: taskState,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return LongPressDraggable<TaskData>(
|
||||
data: TaskData(widget.task, _cardSize),
|
||||
maxSimultaneousDrags: taskState.taskDragging ? 0 : 1, // only one task can be dragged at a time
|
||||
onDragStarted: () {
|
||||
taskState.taskDragging = true;
|
||||
setState(() => _dragging = true);
|
||||
},
|
||||
onDragUpdate: widget.onDragUpdate,
|
||||
onDragEnd: (_) {
|
||||
taskState.taskDragging = false;
|
||||
setState(() => _dragging = false);
|
||||
},
|
||||
feedback: (_cardSize == null) ? SizedBox.shrink() : SizedBox.fromSize(
|
||||
size: _cardSize,
|
||||
child: Card(
|
||||
color: card.color,
|
||||
child: (card.child as InkWell).child,
|
||||
elevation: (card.elevation ?? 0) + 5,
|
||||
),
|
||||
),
|
||||
childWhenDragging: SizedBox.shrink(),
|
||||
child: () {
|
||||
if (_dragging || _cardSize == null) return card;
|
||||
|
||||
final dropBoxSize = _dropData?.size ?? _cardSize;
|
||||
final dropBox = DottedBorder(
|
||||
color: Colors.white,
|
||||
child: SizedBox.fromSize(size: dropBoxSize),
|
||||
);
|
||||
final dropAbove = taskState.taskDragging && _dropLocation == DropLocation.above;
|
||||
final dropBelow = taskState.taskDragging && _dropLocation == DropLocation.below;
|
||||
final DragTargetLeave<TaskData> dragTargetOnLeave = (data) => setState(() {
|
||||
_dropLocation = DropLocation.none;
|
||||
_dropData = null;
|
||||
});
|
||||
final DragTargetAccept<TaskData> dragTargetOnAccept = (data) {
|
||||
final index = bucket.tasks.indexOf(widget.task);
|
||||
widget.onAccept(data.task, _dropLocation == DropLocation.above ? index : index + 1);
|
||||
setState(() {
|
||||
_dropLocation = DropLocation.none;
|
||||
_dropData = null;
|
||||
});
|
||||
};
|
||||
|
||||
return SizedBox(
|
||||
width: _cardSize.width,
|
||||
height: _cardSize.height + (dropAbove || dropBelow ? dropBoxSize.height + 4: 0),
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: [
|
||||
if (dropAbove) dropBox,
|
||||
card,
|
||||
if (dropBelow) dropBox,
|
||||
],
|
||||
),
|
||||
Column(
|
||||
children: <SizedBox>[
|
||||
SizedBox(
|
||||
height: (_cardSize.height / 2) + (dropAbove ? dropBoxSize.height : 0),
|
||||
child: DragTarget<TaskData>(
|
||||
onWillAccept: (data) {
|
||||
setState(() {
|
||||
_dropLocation = DropLocation.above;
|
||||
_dropData = data;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
onAccept: dragTargetOnAccept,
|
||||
onLeave: dragTargetOnLeave,
|
||||
builder: (_, __, ___) => SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: (_cardSize.height / 2) + (dropBelow ? dropBoxSize.height : 0),
|
||||
child: DragTarget<TaskData>(
|
||||
onWillAccept: (data) {
|
||||
setState(() {
|
||||
_dropLocation = DropLocation.below;
|
||||
_dropData = data;
|
||||
});
|
||||
return true;
|
||||
},
|
||||
onAccept: dragTargetOnAccept,
|
||||
onLeave: dragTargetOnLeave,
|
||||
builder: (_, __, ___) => SizedBox.expand(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}(),
|
||||
);
|
||||
}
|
||||
|
||||
void _updateCardSize(BuildContext context) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() {
|
||||
_cardSize = context.size;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => _currentTask != widget.task;
|
||||
}
|
||||
bool get wantKeepAlive => _dragging;
|
||||
}
|
|
@ -1,27 +1,48 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:vikunja_app/components/BucketTaskCard.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/stores/list_store.dart';
|
||||
|
||||
class SliverBucketList extends StatelessWidget {
|
||||
final Bucket bucket;
|
||||
final Function onLast;
|
||||
final DragUpdateCallback onTaskDragUpdate;
|
||||
|
||||
const SliverBucketList({Key key, @required this.bucket, this.onLast})
|
||||
: assert(bucket != null),
|
||||
super(key: key);
|
||||
const SliverBucketList({
|
||||
Key key,
|
||||
@required this.bucket,
|
||||
@required this.onTaskDragUpdate,
|
||||
}) : assert(bucket != null),
|
||||
assert(onTaskDragUpdate != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
if (bucket.tasks == null) return null;
|
||||
return index < bucket.tasks.length
|
||||
? BucketTaskCard(task: bucket.tasks[index])
|
||||
: () {
|
||||
if (onLast != null) onLast();
|
||||
return null;
|
||||
}();
|
||||
return index >= bucket.tasks.length ? null : BucketTaskCard(
|
||||
key: ObjectKey(bucket.tasks[index]),
|
||||
task: bucket.tasks[index],
|
||||
index: index,
|
||||
onDragUpdate: onTaskDragUpdate,
|
||||
onAccept: (task, index) {
|
||||
_moveTaskToBucket(context, task, index);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _moveTaskToBucket(BuildContext context, Task task, int index) {
|
||||
return Provider.of<ListProvider>(context, listen: false).moveTaskToBucket(
|
||||
context: context,
|
||||
task: task,
|
||||
newBucketId: bucket.id,
|
||||
index: index,
|
||||
).then((_) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('${task.title} was moved to ${bucket.title} successfully!'),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/utils/misc.dart';
|
||||
|
||||
import '../pages/list/task_edit.dart';
|
||||
import 'package:vikunja_app/pages/list/task_edit.dart';
|
||||
import 'package:vikunja_app/stores/list_store.dart';
|
||||
|
||||
class TaskTile extends StatefulWidget {
|
||||
final Task task;
|
||||
|
@ -84,8 +84,9 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
|
|||
Navigator.push<Task>(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => TaskEditPage(
|
||||
builder: (buildContext) => TaskEditPage(
|
||||
task: _currentTask,
|
||||
taskState: Provider.of<ListProvider>(context),
|
||||
),
|
||||
),
|
||||
).then((task) => setState(() {
|
||||
|
@ -109,9 +110,12 @@ class TaskTileState extends State<TaskTile> with AutomaticKeepAliveClientMixin {
|
|||
}
|
||||
|
||||
Future<Task> _updateTask(Task task, bool checked) {
|
||||
return VikunjaGlobal.of(context).taskService.update(task.copyWith(
|
||||
done: checked,
|
||||
));
|
||||
return Provider.of<ListProvider>(context, listen: false).updateTask(
|
||||
context: context,
|
||||
task: task.copyWith(
|
||||
done: checked,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -44,7 +44,7 @@ class Bucket {
|
|||
tasks = (json['tasks'] as List<dynamic>)
|
||||
?.map((task) => Task.fromJson(task))
|
||||
?.cast<Task>()
|
||||
?.toList();
|
||||
?.toList() ?? <Task>[];
|
||||
|
||||
toJSON() => {
|
||||
'id': id,
|
||||
|
|
|
@ -5,12 +5,14 @@ import 'dart:ui';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:dotted_border/dotted_border.dart';
|
||||
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
|
||||
import 'package:vikunja_app/components/AddDialog.dart';
|
||||
import 'package:vikunja_app/components/TaskTile.dart';
|
||||
import 'package:vikunja_app/components/SliverBucketList.dart';
|
||||
import 'package:vikunja_app/components/SliverBucketPersistentHeader.dart';
|
||||
import 'package:vikunja_app/components/BucketLimitDialog.dart';
|
||||
import 'package:vikunja_app/components/BucketTaskCard.dart';
|
||||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/list.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
|
@ -22,11 +24,12 @@ import 'package:vikunja_app/stores/list_store.dart';
|
|||
enum BucketMenu {limit, done, delete}
|
||||
|
||||
class BucketProps {
|
||||
final ValueKey<int> key;
|
||||
bool scrollable = false;
|
||||
final ScrollController controller = ScrollController();
|
||||
final TextEditingController titleController = TextEditingController();
|
||||
BucketProps(this.key);
|
||||
bool scrollable = false;
|
||||
bool portrait = true;
|
||||
int bucketLength = 0;
|
||||
Size taskDropSize;
|
||||
}
|
||||
|
||||
class ListPage extends StatefulWidget {
|
||||
|
@ -40,7 +43,7 @@ class ListPage extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ListPageState extends State<ListPage> {
|
||||
final _globalKey = GlobalKey();
|
||||
final _keyboardController = KeyboardVisibilityController();
|
||||
int _viewIndex = 0;
|
||||
TaskList _list;
|
||||
List<Task> _loadingTasks = [];
|
||||
|
@ -51,6 +54,7 @@ class _ListPageState extends State<ListPage> {
|
|||
PageController _pageController;
|
||||
Map<int, BucketProps> _bucketProps = {};
|
||||
int _draggedBucketIndex;
|
||||
Duration _lastTaskDragUpdateAction = Duration.zero;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -59,6 +63,9 @@ class _ListPageState extends State<ListPage> {
|
|||
title: widget.taskList.title,
|
||||
tasks: [],
|
||||
);
|
||||
_keyboardController.onChange.listen((visible) {
|
||||
if (!visible && mounted) FocusScope.of(context).unfocus();
|
||||
});
|
||||
super.initState();
|
||||
Future.delayed(Duration.zero, (){
|
||||
_loadList();
|
||||
|
@ -68,13 +75,7 @@ class _ListPageState extends State<ListPage> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
taskState = Provider.of<ListProvider>(context);
|
||||
KeyboardVisibilityController().onChange.listen((visible) {
|
||||
if (!visible) try {
|
||||
FocusScope.of(_globalKey.currentContext ?? context).unfocus();
|
||||
} catch (e) {}
|
||||
});
|
||||
return GestureDetector(
|
||||
key: _globalKey,
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
|
@ -190,25 +191,31 @@ class _ListPageState extends State<ListPage> {
|
|||
|
||||
Widget _kanbanView(BuildContext context) {
|
||||
final deviceData = MediaQuery.of(context);
|
||||
final bucketWidth = deviceData.size.width
|
||||
* (deviceData.orientation == Orientation.portrait ? 0.8 : 0.4);
|
||||
if (_pageController == null) _pageController = PageController(viewportFraction: 0.8);
|
||||
final portrait = deviceData.orientation == Orientation.portrait;
|
||||
final bucketFraction = portrait ? 0.8 : 0.4;
|
||||
final bucketWidth = deviceData.size.width * bucketFraction;
|
||||
|
||||
if (_pageController == null) _pageController = PageController(viewportFraction: bucketFraction);
|
||||
else if (_pageController.viewportFraction != bucketFraction)
|
||||
_pageController = PageController(viewportFraction: bucketFraction);
|
||||
|
||||
return ReorderableListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
scrollController: _pageController,
|
||||
physics: PageScrollPhysics(),
|
||||
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,
|
||||
itemCount: taskState.buckets.length,
|
||||
itemExtent: bucketWidth,
|
||||
cacheExtent: bucketWidth,
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) {
|
||||
if (index > taskState.buckets.length) return null;
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ValueKey<int>(index),
|
||||
index: index,
|
||||
enabled: taskState.buckets.length > 1,
|
||||
child: _buildBucketTile(taskState.buckets[index]),
|
||||
enabled: taskState.buckets.length > 1 && !taskState.taskDragging,
|
||||
child: SizedBox(
|
||||
width: bucketWidth,
|
||||
child: _buildBucketTile(taskState.buckets[index], portrait),
|
||||
),
|
||||
);
|
||||
},
|
||||
proxyDecorator: (child, index, animation) {
|
||||
|
@ -224,26 +231,29 @@ class _ListPageState extends State<ListPage> {
|
|||
);
|
||||
},
|
||||
footer: _draggedBucketIndex != null ? null : SizedBox(
|
||||
width: bucketWidth,
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _addBucketDialog(context),
|
||||
label: Text('Create Bucket'),
|
||||
//style: ButtonStyle(alignment: Alignment.centerLeft),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
width: deviceData.size.width * (1 - bucketFraction) * (portrait ? 1 : 2),
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: portrait ? 14 : 5,
|
||||
),
|
||||
child: RotatedBox(
|
||||
quarterTurns: portrait ? 1 : 0,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _addBucketDialog(context),
|
||||
label: Text('Create Bucket'),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onReorderStart: (oldIndex) => setState(() => _draggedBucketIndex = oldIndex),
|
||||
onReorder: (oldIndex, newIndex) {},
|
||||
onReorderStart: (oldIndex) {
|
||||
FocusScope.of(context).unfocus();
|
||||
setState(() => _draggedBucketIndex = oldIndex);
|
||||
},
|
||||
onReorder: (_, __) {},
|
||||
onReorderEnd: (newIndex) => setState(() {
|
||||
bool indexUpdated = false;
|
||||
if (newIndex > _draggedBucketIndex) {
|
||||
|
@ -257,78 +267,79 @@ class _ListPageState extends State<ListPage> {
|
|||
newIndex = 1;
|
||||
}
|
||||
taskState.buckets[newIndex].position = newIndex == taskState.buckets.length - 1
|
||||
? taskState.buckets[newIndex - 1].position + 1
|
||||
? taskState.buckets[newIndex - 1].position + pow(2.0, 16.0)
|
||||
: (taskState.buckets[newIndex - 1].position
|
||||
+ taskState.buckets[newIndex + 1].position) / 2.0;
|
||||
_updateBucket(context, taskState.buckets[newIndex]);
|
||||
_draggedBucketIndex = null;
|
||||
if (indexUpdated)
|
||||
_pageController.jumpToPage((newIndex
|
||||
/ (deviceData.orientation == Orientation.portrait ? 1 : 2)).floor());
|
||||
if (indexUpdated && portrait) _pageController.animateToPage(
|
||||
newIndex - 1,
|
||||
duration: Duration(milliseconds: 100),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
TaskTile _buildTile(Task task) {
|
||||
return TaskTile(
|
||||
task: task,
|
||||
loading: false,
|
||||
onEdit: () {
|
||||
/*Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
TaskEditPage(
|
||||
task: task,
|
||||
),
|
||||
),
|
||||
);*/
|
||||
},
|
||||
onMarkedAsDone: (done) {
|
||||
Provider.of<ListProvider>(context, listen: false).updateTask(
|
||||
context: context,
|
||||
id: task.id,
|
||||
done: done,
|
||||
);
|
||||
},
|
||||
Widget _buildTile(Task task) {
|
||||
return ListenableProvider.value(
|
||||
value: taskState,
|
||||
child: TaskTile(
|
||||
task: task,
|
||||
loading: false,
|
||||
onEdit: () {},
|
||||
onMarkedAsDone: (done) {
|
||||
Provider.of<ListProvider>(context, listen: false).updateTask(
|
||||
context: context,
|
||||
task: task.copyWith(done: done),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBucketTile(Bucket bucket) {
|
||||
Widget _buildBucketTile(Bucket bucket, bool portrait) {
|
||||
final theme = Theme.of(context);
|
||||
const bucketTitleHeight = 56.0;
|
||||
final addTaskButton = ElevatedButton.icon(
|
||||
icon: Icon(Icons.add),
|
||||
label: Text('Add Task'),
|
||||
onPressed: (bucket.tasks?.length ?? -1) < bucket.limit
|
||||
? () => _addItemDialog(context, bucket)
|
||||
onPressed: bucket.limit == 0 || bucket.tasks.length < bucket.limit
|
||||
? () {
|
||||
FocusScope.of(context).unfocus();
|
||||
_addItemDialog(context, bucket);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
if (_bucketProps[bucket.id] == null) {
|
||||
_bucketProps[bucket.id] = BucketProps(ValueKey<int>(bucket.id));
|
||||
if (_bucketProps[bucket.id] == null)
|
||||
_bucketProps[bucket.id] = BucketProps();
|
||||
if (_bucketProps[bucket.id].bucketLength != (bucket.tasks.length)
|
||||
|| _bucketProps[bucket.id].portrait != portrait)
|
||||
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {
|
||||
if (_bucketProps[bucket.id].controller.hasClients) setState(() {
|
||||
_bucketProps[bucket.id].bucketLength = bucket.tasks.length;
|
||||
_bucketProps[bucket.id].scrollable = _bucketProps[bucket.id].controller.position.maxScrollExtent > 0;
|
||||
_bucketProps[bucket.id].portrait = portrait;
|
||||
});
|
||||
});
|
||||
}
|
||||
if (_bucketProps[bucket.id].titleController.text.isEmpty)
|
||||
_bucketProps[bucket.id].titleController.text = bucket.title;
|
||||
|
||||
return Stack(
|
||||
key: _bucketProps[bucket.id].key,
|
||||
children: <Widget>[
|
||||
CustomScrollView(
|
||||
controller: _bucketProps[bucket.id].controller,
|
||||
slivers: <Widget>[
|
||||
SliverBucketPersistentHeader(
|
||||
minExtent: 56,
|
||||
maxExtent: 56,
|
||||
minExtent: bucketTitleHeight,
|
||||
maxExtent: bucketTitleHeight,
|
||||
child: Material(
|
||||
color: theme.scaffoldBackgroundColor,
|
||||
child: ListTile(
|
||||
minLeadingWidth: 15,
|
||||
horizontalTitleGap: 4,
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 10),
|
||||
leading: bucket.isDoneBucket ? Icon(
|
||||
Icons.done_all,
|
||||
color: Colors.green,
|
||||
|
@ -357,75 +368,87 @@ class _ListPageState extends State<ListPage> {
|
|||
},
|
||||
),
|
||||
),
|
||||
if (bucket.limit != 0)
|
||||
Text(
|
||||
'${bucket.tasks?.length ?? 0}/${bucket.limit}',
|
||||
if (bucket.limit != 0) Padding(
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
child: Text(
|
||||
'${bucket.tasks.length}/${bucket.limit}',
|
||||
style: theme.textTheme.titleMedium.copyWith(
|
||||
color: (bucket.tasks?.length ?? -1) >= bucket.limit
|
||||
color: bucket.limit != 0 && bucket.tasks.length >= bucket.limit
|
||||
? Colors.red : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton<BucketMenu>(
|
||||
child: Icon(Icons.more_vert),
|
||||
onSelected: (item) {
|
||||
switch (item) {
|
||||
case BucketMenu.limit:
|
||||
showDialog<int>(context: context,
|
||||
builder: (_) => BucketLimitDialog(
|
||||
bucket: bucket,
|
||||
),
|
||||
).then((limit) {
|
||||
if (limit != null) {
|
||||
bucket.limit = limit;
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(Icons.drag_handle),
|
||||
PopupMenuButton<BucketMenu>(
|
||||
child: Icon(Icons.more_vert),
|
||||
onSelected: (item) {
|
||||
switch (item) {
|
||||
case BucketMenu.limit:
|
||||
showDialog<int>(context: context,
|
||||
builder: (_) => BucketLimitDialog(
|
||||
bucket: bucket,
|
||||
),
|
||||
).then((limit) {
|
||||
if (limit != null) {
|
||||
bucket.limit = limit;
|
||||
_updateBucket(context, bucket);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case BucketMenu.done:
|
||||
bucket.isDoneBucket = !bucket.isDoneBucket;
|
||||
_updateBucket(context, bucket);
|
||||
}
|
||||
});
|
||||
break;
|
||||
case BucketMenu.done:
|
||||
bucket.isDoneBucket = !bucket.isDoneBucket;
|
||||
_updateBucket(context, bucket);
|
||||
break;
|
||||
case BucketMenu.delete:
|
||||
_deleteBucket(context, bucket);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => <PopupMenuEntry<BucketMenu>>[
|
||||
PopupMenuItem<BucketMenu>(
|
||||
value: BucketMenu.limit,
|
||||
child: Text('Limit: ${bucket.limit}'),
|
||||
),
|
||||
PopupMenuItem<BucketMenu>(
|
||||
value: BucketMenu.done,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
Icons.done_all,
|
||||
color: bucket.isDoneBucket ? Colors.green : null,
|
||||
break;
|
||||
case BucketMenu.delete:
|
||||
_deleteBucket(context, bucket);
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) {
|
||||
final enableDelete = taskState.buckets.length > 1;
|
||||
return <PopupMenuEntry<BucketMenu>>[
|
||||
PopupMenuItem<BucketMenu>(
|
||||
value: BucketMenu.limit,
|
||||
child: Text('Limit: ${bucket.limit}'),
|
||||
),
|
||||
PopupMenuItem<BucketMenu>(
|
||||
value: BucketMenu.done,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4),
|
||||
child: Icon(
|
||||
Icons.done_all,
|
||||
color: bucket.isDoneBucket ? Colors.green : null,
|
||||
),
|
||||
),
|
||||
Text('Done Bucket'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text('Done Bucket'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem<BucketMenu>(
|
||||
value: BucketMenu.delete,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: Colors.red,
|
||||
const PopupMenuDivider(),
|
||||
PopupMenuItem<BucketMenu>(
|
||||
value: BucketMenu.delete,
|
||||
enabled: enableDelete,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.delete,
|
||||
color: enableDelete ? Colors.red : null,
|
||||
),
|
||||
Text(
|
||||
'Delete',
|
||||
style: enableDelete ? TextStyle(color: Colors.red) : null,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Delete',
|
||||
style: TextStyle(color: Colors.red),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -434,8 +457,42 @@ class _ListPageState extends State<ListPage> {
|
|||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
sliver: SliverBucketList(
|
||||
bucket: bucket,
|
||||
sliver: ListenableProvider.value(
|
||||
value: taskState,
|
||||
child: SliverBucketList(
|
||||
bucket: bucket,
|
||||
onTaskDragUpdate: (details) { // scroll when dragging a task
|
||||
if (details.sourceTimeStamp - _lastTaskDragUpdateAction > const Duration(milliseconds: 600)) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
const scrollDuration = Duration(milliseconds: 250);
|
||||
const scrollCurve = Curves.easeInOut;
|
||||
final updateAction = () => setState(() => _lastTaskDragUpdateAction = details.sourceTimeStamp);
|
||||
if (details.globalPosition.dx < screenSize.width * 0.1) { // scroll left
|
||||
if (_pageController.position.extentBefore != 0)
|
||||
_pageController.previousPage(duration: scrollDuration, curve: scrollCurve);
|
||||
updateAction();
|
||||
} else if (details.globalPosition.dx > screenSize.width * 0.9) { // scroll right
|
||||
if (_pageController.position.extentAfter != 0)
|
||||
_pageController.nextPage(duration: scrollDuration, curve: scrollCurve);
|
||||
updateAction();
|
||||
} else {
|
||||
final viewingBucket = taskState.buckets[_pageController.page.floor()];
|
||||
final bucketController = _bucketProps[viewingBucket.id].controller;
|
||||
if (details.globalPosition.dy < screenSize.height * 0.2) { // scroll up
|
||||
if (bucketController.position.extentBefore != 0)
|
||||
bucketController.animateTo(bucketController.offset - 80,
|
||||
duration: scrollDuration, curve: scrollCurve);
|
||||
updateAction();
|
||||
} else if (details.globalPosition.dy > screenSize.height * 0.8) { // scroll down
|
||||
if (bucketController.position.extentAfter != 0)
|
||||
bucketController.animateTo(bucketController.offset + 80,
|
||||
duration: scrollDuration, curve: scrollCurve);
|
||||
updateAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverVisibility(
|
||||
|
@ -445,9 +502,41 @@ class _ListPageState extends State<ListPage> {
|
|||
maintainSize: true,
|
||||
sliver: SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: addTaskButton,
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Column(
|
||||
children: <Widget>[
|
||||
if (_bucketProps[bucket.id].taskDropSize != null) DottedBorder(
|
||||
color: Colors.white,
|
||||
child: SizedBox.fromSize(size: _bucketProps[bucket.id].taskDropSize),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: addTaskButton,
|
||||
),
|
||||
],
|
||||
),
|
||||
// DragTarget to drop tasks in empty buckets
|
||||
if (bucket.tasks.length == 0) DragTarget<TaskData>(
|
||||
onWillAccept: (data) {
|
||||
setState(() => _bucketProps[bucket.id].taskDropSize = data.size);
|
||||
return true;
|
||||
},
|
||||
onAccept: (data) {
|
||||
Provider.of<ListProvider>(context, listen: false).moveTaskToBucket(
|
||||
context: context,
|
||||
task: data.task,
|
||||
newBucketId: bucket.id,
|
||||
index: 0,
|
||||
).then((_) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('${data.task.title} was moved to ${bucket.title} successfully!'),
|
||||
)));
|
||||
setState(() => _bucketProps[bucket.id].taskDropSize = null);
|
||||
},
|
||||
onLeave: (_) => setState(() => _bucketProps[bucket.id].taskDropSize = null),
|
||||
builder: (_, __, ___) => SizedBox.expand(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -475,6 +564,7 @@ class _ListPageState extends State<ListPage> {
|
|||
MaterialPageRoute(
|
||||
builder: (context) => TaskEditPage(
|
||||
task: task,
|
||||
taskState: taskState,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -602,8 +692,9 @@ class _ListPageState extends State<ListPage> {
|
|||
}
|
||||
|
||||
_deleteBucket(BuildContext context, Bucket bucket) {
|
||||
if ((bucket.tasks?.length ?? 0) > 0) {
|
||||
int defaultBucketId = taskState.buckets[0]?.id ?? 100;
|
||||
// Move bucket's tasks to default bucket (the one with the lowest id)
|
||||
if (bucket.tasks.length > 0) {
|
||||
int defaultBucketId = taskState.buckets[0].id;
|
||||
taskState.buckets.forEach((b) {
|
||||
if (b.id < defaultBucketId)
|
||||
defaultBucketId = b.id;
|
||||
|
|
|
@ -8,12 +8,19 @@ import 'package:vikunja_app/components/label.dart';
|
|||
import 'package:vikunja_app/global.dart';
|
||||
import 'package:vikunja_app/models/label.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/stores/list_store.dart';
|
||||
import 'package:vikunja_app/utils/repeat_after_parse.dart';
|
||||
|
||||
class TaskEditPage extends StatefulWidget {
|
||||
final Task task;
|
||||
final ListProvider taskState;
|
||||
|
||||
TaskEditPage({this.task}) : super(key: Key(task.toString()));
|
||||
TaskEditPage({
|
||||
@required this.task,
|
||||
@required this.taskState,
|
||||
}) : assert(task != null),
|
||||
assert(taskState != null),
|
||||
super(key: Key(task.toString()));
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _TaskEditPageState();
|
||||
|
@ -381,7 +388,10 @@ class _TaskEditPageState extends State<TaskEditPage> {
|
|||
);
|
||||
});
|
||||
|
||||
VikunjaGlobal.of(context).taskService.update(updatedTask).then((result) {
|
||||
widget.taskState.updateTask(
|
||||
context: context,
|
||||
task: updatedTask,
|
||||
).then((result) {
|
||||
setState(() { _loading = false; _changed = false;});
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('The task was updated successfully!'),
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:vikunja_app/models/task.dart';
|
||||
import 'package:vikunja_app/models/bucket.dart';
|
||||
|
@ -5,6 +7,7 @@ import 'package:vikunja_app/global.dart';
|
|||
|
||||
class ListProvider with ChangeNotifier {
|
||||
bool _isLoading = false;
|
||||
bool _taskDragging = false;
|
||||
int _maxPages = 0;
|
||||
|
||||
// TODO: Streams
|
||||
|
@ -13,6 +16,13 @@ class ListProvider with ChangeNotifier {
|
|||
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
bool get taskDragging => _taskDragging;
|
||||
|
||||
set taskDragging(bool value) {
|
||||
_taskDragging = value;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
int get maxPages => _maxPages;
|
||||
|
||||
set tasks(List<Task> tasks) {
|
||||
|
@ -117,14 +127,8 @@ class ListProvider with ChangeNotifier {
|
|||
});
|
||||
}
|
||||
|
||||
void updateTask({BuildContext context, int id, bool done}) {
|
||||
var globalState = VikunjaGlobal.of(context);
|
||||
globalState.taskService
|
||||
.update(Task(
|
||||
id: id,
|
||||
done: done,
|
||||
))
|
||||
.then((task) {
|
||||
Future<Task> updateTask({BuildContext context, Task task}) {
|
||||
return VikunjaGlobal.of(context).taskService.update(task).then((task) {
|
||||
// FIXME: This is ugly. We should use a redux to not have to do these kind of things.
|
||||
// This is enough for now (it works™) but we should definitly fix it later.
|
||||
_tasks.asMap().forEach((i, t) {
|
||||
|
@ -132,7 +136,13 @@ class ListProvider with ChangeNotifier {
|
|||
_tasks[i] = task;
|
||||
}
|
||||
});
|
||||
_buckets.asMap().forEach((i, b) => b.tasks.asMap().forEach((v, t) {
|
||||
if (task.id == t.id){
|
||||
_buckets[i].tasks[v] = task;
|
||||
}
|
||||
}));
|
||||
notifyListeners();
|
||||
return task;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -161,4 +171,60 @@ class ListProvider with ChangeNotifier {
|
|||
notifyListeners();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> moveTaskToBucket({BuildContext context, Task task, int newBucketId, int index}) async {
|
||||
final sameBucket = task.bucketId == newBucketId;
|
||||
final newBucketIndex = _buckets.indexWhere((b) => b.id == newBucketId);
|
||||
if (sameBucket && index > _buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)) index--;
|
||||
|
||||
_buckets[_buckets.indexWhere((b) => b.id == task.bucketId)].tasks.remove(task);
|
||||
if (index >= _buckets[newBucketIndex].tasks.length)
|
||||
_buckets[newBucketIndex].tasks.add(task);
|
||||
else
|
||||
_buckets[newBucketIndex].tasks.insert(index, task);
|
||||
|
||||
double kanbanPosition;
|
||||
if (_buckets[newBucketIndex].tasks.length == 1) // only task
|
||||
kanbanPosition = 0.0;
|
||||
else if (index == 0) // first task
|
||||
kanbanPosition = _buckets[newBucketIndex].tasks[1].kanbanPosition / 2.0;
|
||||
else if (index == _buckets[newBucketIndex].tasks.length - 1) // last task
|
||||
kanbanPosition = _buckets[newBucketIndex].tasks[index - 1].kanbanPosition + pow(2.0, 16.0);
|
||||
else // in the middle
|
||||
kanbanPosition = (_buckets[newBucketIndex].tasks[index - 1].kanbanPosition
|
||||
+ _buckets[newBucketIndex].tasks[index + 1].kanbanPosition) / 2.0;
|
||||
|
||||
task = await VikunjaGlobal.of(context).taskService.update(task.copyWith(
|
||||
bucketId: newBucketId ?? task.bucketId,
|
||||
kanbanPosition: kanbanPosition,
|
||||
));
|
||||
_buckets[newBucketIndex].tasks[index] = task;
|
||||
|
||||
// make sure first 2 tasks don't have 0 kanbanPosition
|
||||
Task secondTask;
|
||||
if (index == 0 && _buckets[newBucketIndex].tasks.length > 1
|
||||
&& _buckets[newBucketIndex].tasks[1].kanbanPosition == 0) {
|
||||
if (_buckets[newBucketIndex].tasks.length == 2) // last task
|
||||
kanbanPosition = _buckets[newBucketIndex].tasks[0].kanbanPosition + pow(2.0, 16.0);
|
||||
else // in the middle
|
||||
kanbanPosition = (_buckets[newBucketIndex].tasks[0].kanbanPosition
|
||||
+ _buckets[newBucketIndex].tasks[2].kanbanPosition) / 2.0;
|
||||
|
||||
secondTask = await VikunjaGlobal.of(context).taskService.update(
|
||||
_buckets[newBucketIndex].tasks[1].copyWith(
|
||||
kanbanPosition: kanbanPosition,
|
||||
));
|
||||
_buckets[newBucketIndex].tasks[1] = secondTask;
|
||||
}
|
||||
|
||||
if (_tasks.isNotEmpty) {
|
||||
_tasks[_tasks.indexWhere((t) => t.id == task.id)] = task;
|
||||
if (secondTask != null) _tasks[_tasks.indexWhere((t) => t.id == secondTask.id)] = secondTask;
|
||||
}
|
||||
|
||||
_buckets[newBucketIndex].tasks[_buckets[newBucketIndex].tasks.indexWhere((t) => t.id == task.id)] = task;
|
||||
_buckets[newBucketIndex].tasks.sort((a, b) => a.kanbanPosition.compareTo(b.kanbanPosition));
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
|
21
pubspec.lock
21
pubspec.lock
|
@ -148,6 +148,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.6"
|
||||
dotted_border:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dotted_border
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.0+2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -436,6 +443,20 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.1"
|
||||
path_drawing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_drawing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
petitparser:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -26,6 +26,7 @@ dependencies:
|
|||
webview_flutter: ^3.0.4
|
||||
flutter_colorpicker: ^1.0.3
|
||||
flutter_keyboard_visibility: ^5.3.0
|
||||
dotted_border: ^2.0.0+2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue
Block a user