Add linux deb dependencies Add backup recovery strategy select Support custom text scaling Optimize the display of different text scale Optimize windows setup experience Optimize startTun performance Optimize android tv experience Optimize default option Optimize computed text size Optimize hyperOS freeform window Add developer mode Update core Optimize more details
894 lines
23 KiB
Dart
894 lines
23 KiB
Dart
import 'dart:async';
|
|
import 'dart:math';
|
|
|
|
import 'package:defer_pointer/defer_pointer.dart';
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/widgets/activate_box.dart';
|
|
import 'package:fl_clash/widgets/card.dart';
|
|
import 'package:fl_clash/widgets/grid.dart';
|
|
import 'package:fl_clash/widgets/sheet.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/physics.dart';
|
|
|
|
typedef VoidCallback = void Function();
|
|
|
|
class SuperGrid extends StatefulWidget {
|
|
final List<GridItem> children;
|
|
final double mainAxisSpacing;
|
|
final double crossAxisSpacing;
|
|
final int crossAxisCount;
|
|
final void Function(List<GridItem> newChildren)? onSave;
|
|
final List<GridItem> Function(List<GridItem> newChildren)? addedItemsBuilder;
|
|
|
|
const SuperGrid({
|
|
super.key,
|
|
required this.children,
|
|
this.crossAxisCount = 1,
|
|
this.mainAxisSpacing = 0,
|
|
this.crossAxisSpacing = 0,
|
|
this.onSave,
|
|
this.addedItemsBuilder,
|
|
});
|
|
|
|
@override
|
|
State<SuperGrid> createState() => SuperGridState();
|
|
}
|
|
|
|
class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
|
|
final ValueNotifier<List<GridItem>> _childrenNotifier = ValueNotifier([]);
|
|
final ValueNotifier<List<GridItem>> addedChildrenNotifier = ValueNotifier([]);
|
|
|
|
int get length => _childrenNotifier.value.length;
|
|
List<int> _tempIndexList = [];
|
|
List<BuildContext?> _itemContexts = [];
|
|
Size _containerSize = Size.zero;
|
|
int _targetIndex = -1;
|
|
Offset _targetOffset = Offset.zero;
|
|
List<Size> _sizes = [];
|
|
List<Offset> _offsets = [];
|
|
Offset _parentOffset = Offset.zero;
|
|
EdgeDraggingAutoScroller? _edgeDraggingAutoScroller;
|
|
final ValueNotifier<bool> isEditNotifier = ValueNotifier(false);
|
|
|
|
Map<int, Tween<Offset>> _transformTweenMap = {};
|
|
|
|
final ValueNotifier<bool> _animating = ValueNotifier(false);
|
|
|
|
final _dragWidgetSizeNotifier = ValueNotifier(Size.zero);
|
|
final _dragIndexNotifier = ValueNotifier(-1);
|
|
|
|
late AnimationController _transformController;
|
|
|
|
Completer? _transformCompleter;
|
|
|
|
Map<int, Animation<Offset>> _transformAnimationMap = {};
|
|
|
|
late AnimationController _fakeDragWidgetController;
|
|
Animation<Offset>? _fakeDragWidgetAnimation;
|
|
|
|
late AnimationController _shakeController;
|
|
late Animation<double> _shakeAnimation;
|
|
Rect _dragRect = Rect.zero;
|
|
Scrollable? _scrollable;
|
|
|
|
int get crossCount => widget.crossAxisCount;
|
|
|
|
_onChildrenChange() {
|
|
_tempIndexList = List.generate(length, (index) => index);
|
|
_itemContexts = List.filled(
|
|
length,
|
|
null,
|
|
);
|
|
}
|
|
|
|
_preTransformState() {
|
|
_sizes = _itemContexts.map((item) => item!.size!).toList();
|
|
_parentOffset =
|
|
(context.findRenderObject() as RenderBox).localToGlobal(Offset.zero);
|
|
_offsets = _itemContexts
|
|
.map((item) =>
|
|
(item!.findRenderObject() as RenderBox).localToGlobal(Offset.zero) -
|
|
_parentOffset)
|
|
.toList();
|
|
_containerSize = context.size!;
|
|
}
|
|
|
|
showAddModal() {
|
|
if (!isEditNotifier.value) {
|
|
return;
|
|
}
|
|
showSheet(
|
|
builder: (_, type) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: addedChildrenNotifier,
|
|
builder: (_, value, __) {
|
|
return AdaptiveSheetScaffold(
|
|
type: type,
|
|
body: _AddedWidgetsModal(
|
|
items: value,
|
|
onAdd: (gridItem) {
|
|
_childrenNotifier.value = List.from(_childrenNotifier.value)
|
|
..add(
|
|
gridItem,
|
|
);
|
|
},
|
|
),
|
|
title: appLocalizations.add,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
context: context,
|
|
);
|
|
}
|
|
|
|
_initState() {
|
|
_transformController.value = 0;
|
|
_sizes = List.generate(length, (index) => Size.zero);
|
|
_offsets = [];
|
|
_transformTweenMap.clear();
|
|
_transformAnimationMap.clear();
|
|
_containerSize = Size.zero;
|
|
_dragIndexNotifier.value = -1;
|
|
_dragWidgetSizeNotifier.value = Size.zero;
|
|
_targetOffset = Offset.zero;
|
|
_parentOffset = Offset.zero;
|
|
_dragRect = Rect.zero;
|
|
_targetIndex = -1;
|
|
}
|
|
|
|
_handleChildrenNotifierChange() {
|
|
addedChildrenNotifier.value = widget.addedItemsBuilder != null
|
|
? widget.addedItemsBuilder!(_childrenNotifier.value)
|
|
: [];
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
_childrenNotifier.value = widget.children;
|
|
|
|
_childrenNotifier.addListener(_handleChildrenNotifierChange);
|
|
|
|
isEditNotifier.addListener(_handleIsEditChange);
|
|
|
|
_fakeDragWidgetController = AnimationController.unbounded(
|
|
vsync: this,
|
|
duration: commonDuration,
|
|
);
|
|
|
|
_shakeController = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 120),
|
|
);
|
|
_shakeAnimation = Tween<double>(
|
|
begin: -0.012,
|
|
end: 0.012,
|
|
).animate(
|
|
CurvedAnimation(
|
|
parent: _shakeController,
|
|
curve: Curves.easeInOut,
|
|
),
|
|
);
|
|
|
|
_transformController = AnimationController(
|
|
vsync: this,
|
|
duration: commonDuration,
|
|
);
|
|
|
|
_initState();
|
|
}
|
|
|
|
_handleIsEditChange() async {
|
|
_handleChildrenNotifierChange();
|
|
if (isEditNotifier.value == false) {
|
|
if (widget.onSave != null) {
|
|
await _transformCompleter?.future;
|
|
await Future.delayed(commonDuration);
|
|
widget.onSave!(_childrenNotifier.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
|
|
final scrollable = context.findAncestorWidgetOfExactType<Scrollable>();
|
|
if (scrollable == null) {
|
|
return;
|
|
}
|
|
if (_scrollable != scrollable) {
|
|
_edgeDraggingAutoScroller = EdgeDraggingAutoScroller(
|
|
Scrollable.of(context),
|
|
onScrollViewScrolled: () {
|
|
_edgeDraggingAutoScroller?.startAutoScrollIfNecessary(_dragRect);
|
|
},
|
|
velocityScalar: 40,
|
|
);
|
|
}
|
|
}
|
|
|
|
_transform() async {
|
|
List<Offset> layoutOffsets = [
|
|
Offset(_containerSize.width, 0),
|
|
];
|
|
final List<Offset> nextOffsets = [];
|
|
|
|
for (final index in _tempIndexList) {
|
|
final size = _sizes[index];
|
|
final offset = _getNextOffset(layoutOffsets, size);
|
|
final layoutOffset = Offset(
|
|
min(
|
|
offset.dx + size.width + widget.crossAxisSpacing,
|
|
_containerSize.width,
|
|
),
|
|
min(
|
|
offset.dy + size.height + widget.mainAxisSpacing,
|
|
_containerSize.height,
|
|
),
|
|
);
|
|
final startLayoutOffsetX = offset.dx;
|
|
final endLayoutOffsetX = layoutOffset.dx;
|
|
nextOffsets.add(offset);
|
|
|
|
final startIndex =
|
|
layoutOffsets.indexWhere((i) => i.dx >= startLayoutOffsetX);
|
|
final endIndex =
|
|
layoutOffsets.indexWhere((i) => i.dx >= endLayoutOffsetX);
|
|
final endOffset = layoutOffsets[endIndex];
|
|
|
|
if (startIndex != endIndex) {
|
|
final startOffset = layoutOffsets[startIndex];
|
|
if (startOffset.dx != startLayoutOffsetX) {
|
|
layoutOffsets[startIndex] = Offset(
|
|
startLayoutOffsetX,
|
|
startOffset.dy,
|
|
);
|
|
}
|
|
}
|
|
if (endOffset.dx == endLayoutOffsetX) {
|
|
layoutOffsets[endIndex] = layoutOffset;
|
|
} else {
|
|
layoutOffsets.insert(endIndex, layoutOffset);
|
|
}
|
|
layoutOffsets.removeRange(min(startIndex + 1, endIndex), endIndex);
|
|
}
|
|
|
|
final Map<int, Tween<Offset>> transformTweenMap = {};
|
|
|
|
for (final index in _tempIndexList) {
|
|
final nextIndex = _tempIndexList.indexWhere((i) => i == index);
|
|
transformTweenMap[index] = Tween(
|
|
begin: _transformTweenMap[index]?.begin ?? Offset.zero,
|
|
end: nextOffsets[nextIndex] - _offsets[index],
|
|
);
|
|
}
|
|
|
|
_transformTweenMap = transformTweenMap;
|
|
|
|
_transformAnimationMap = transformTweenMap.map(
|
|
(key, value) {
|
|
final preAnimationValue = _transformAnimationMap[key]?.value;
|
|
return MapEntry(
|
|
key,
|
|
Tween(
|
|
begin: preAnimationValue ?? Offset.zero,
|
|
end: value.end,
|
|
).animate(_transformController),
|
|
);
|
|
},
|
|
);
|
|
|
|
if (_targetIndex != -1) {
|
|
_targetOffset = nextOffsets[_targetIndex];
|
|
}
|
|
return _transformController.forward(from: 0);
|
|
}
|
|
|
|
_handleDragStarted(int index) {
|
|
_initState();
|
|
_preTransformState();
|
|
_dragIndexNotifier.value = index;
|
|
_dragWidgetSizeNotifier.value = _sizes[index];
|
|
_targetIndex = index;
|
|
_targetOffset = _offsets[index];
|
|
_dragRect = Rect.fromLTWH(
|
|
_targetOffset.dx + _parentOffset.dx,
|
|
_targetOffset.dy + _parentOffset.dy,
|
|
_sizes[index].width,
|
|
_sizes[index].height,
|
|
);
|
|
}
|
|
|
|
_handleDragEnd(DraggableDetails details) async {
|
|
debouncer.cancel(DebounceTag.handleWill);
|
|
if (_targetIndex == -1) {
|
|
return;
|
|
}
|
|
const spring = SpringDescription(
|
|
mass: 1,
|
|
stiffness: 100,
|
|
damping: 10,
|
|
);
|
|
final simulation = SpringSimulation(spring, 0, 1, 0);
|
|
_fakeDragWidgetAnimation = Tween(
|
|
begin: details.offset - _parentOffset,
|
|
end: _targetOffset,
|
|
).animate(_fakeDragWidgetController);
|
|
_animating.value = true;
|
|
|
|
_transformCompleter = Completer();
|
|
final animateWith = _fakeDragWidgetController.animateWith(simulation);
|
|
_transformCompleter?.complete(animateWith);
|
|
await animateWith;
|
|
_animating.value = false;
|
|
_fakeDragWidgetAnimation = null;
|
|
_transformTweenMap.clear();
|
|
_transformAnimationMap.clear();
|
|
final children = List<GridItem>.from(_childrenNotifier.value);
|
|
children.insert(_targetIndex, children.removeAt(_dragIndexNotifier.value));
|
|
_childrenNotifier.value = children;
|
|
_initState();
|
|
}
|
|
|
|
_handleDragUpdate(DragUpdateDetails details) {
|
|
_dragRect = _dragRect.translate(
|
|
0,
|
|
details.delta.dy,
|
|
);
|
|
_edgeDraggingAutoScroller?.startAutoScrollIfNecessary(_dragRect);
|
|
}
|
|
|
|
_handleWill(int index) async {
|
|
final dragIndex = _dragIndexNotifier.value;
|
|
if (dragIndex < 0 || dragIndex > _offsets.length - 1) {
|
|
return;
|
|
}
|
|
final targetIndex = _tempIndexList.indexWhere((i) => i == index);
|
|
if (_targetIndex == targetIndex) {
|
|
return;
|
|
}
|
|
_tempIndexList = List.generate(length, (i) {
|
|
if (i == targetIndex) return _dragIndexNotifier.value;
|
|
if (_targetIndex > targetIndex && i > targetIndex && i <= _targetIndex) {
|
|
return _tempIndexList[i - 1];
|
|
} else if (_targetIndex < targetIndex &&
|
|
i >= _targetIndex &&
|
|
i < targetIndex) {
|
|
return _tempIndexList[i + 1];
|
|
}
|
|
return _tempIndexList[i];
|
|
}).toList();
|
|
|
|
_targetIndex = targetIndex;
|
|
|
|
await _transform();
|
|
}
|
|
|
|
_handleDelete(int index) async {
|
|
await _transformCompleter?.future;
|
|
_preTransformState();
|
|
final indexWhere = _tempIndexList.indexWhere((i) => i == index);
|
|
_tempIndexList.removeAt(indexWhere);
|
|
await _transform();
|
|
final children = List<GridItem>.from(_childrenNotifier.value);
|
|
children.removeAt(index);
|
|
_childrenNotifier.value = children;
|
|
_initState();
|
|
}
|
|
|
|
Widget _wrapTransform(Widget rawChild, int index) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: _animating,
|
|
builder: (_, animating, child) {
|
|
if (animating) {
|
|
if (_dragIndexNotifier.value == index) {
|
|
return _sizeBoxWrap(
|
|
Container(),
|
|
index,
|
|
);
|
|
}
|
|
}
|
|
return child!;
|
|
},
|
|
child: AnimatedBuilder(
|
|
builder: (_, child) {
|
|
return Transform.translate(
|
|
offset: _transformAnimationMap[index]?.value ?? Offset.zero,
|
|
child: child,
|
|
);
|
|
},
|
|
animation: _transformController.view,
|
|
child: rawChild,
|
|
),
|
|
);
|
|
}
|
|
|
|
Offset _getNextOffset(List<Offset> offsets, Size size) {
|
|
final length = offsets.length;
|
|
Offset nextOffset = Offset(0, double.infinity);
|
|
for (int i = 0; i < length; i++) {
|
|
final offset = offsets[i];
|
|
if (offset.dy.moreOrEqual(nextOffset.dy)) {
|
|
continue;
|
|
}
|
|
double offsetX = 0;
|
|
double span = 0;
|
|
for (int j = 0;
|
|
span < size.width &&
|
|
j < length &&
|
|
_containerSize.width.moreOrEqual(offsetX + size.width);
|
|
j++) {
|
|
final tempOffset = offsets[j];
|
|
if (offset.dy.moreOrEqual(tempOffset.dy)) {
|
|
span = tempOffset.dx - offsetX;
|
|
if (span.moreOrEqual(size.width)) {
|
|
nextOffset = Offset(offsetX, offset.dy);
|
|
}
|
|
} else {
|
|
offsetX = tempOffset.dx;
|
|
span = 0;
|
|
}
|
|
}
|
|
}
|
|
return nextOffset;
|
|
}
|
|
|
|
Widget _sizeBoxWrap(Widget child, int index) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: _dragWidgetSizeNotifier,
|
|
builder: (_, size, child) {
|
|
return SizedBox.fromSize(
|
|
size: size,
|
|
child: child!,
|
|
);
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget _ignoreWrap(Widget child) {
|
|
return ValueListenableBuilder(
|
|
valueListenable: _animating,
|
|
builder: (_, animating, child) {
|
|
if (animating) {
|
|
return ActivateBox(
|
|
child: child!,
|
|
);
|
|
} else {
|
|
return child!;
|
|
}
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget _shakeWrap(Widget child) {
|
|
final random = 0.7 + Random().nextDouble() * 0.3;
|
|
_shakeController.stop();
|
|
_shakeController.repeat(reverse: true);
|
|
return AnimatedBuilder(
|
|
animation: _shakeAnimation,
|
|
builder: (_, child) {
|
|
return Transform.rotate(
|
|
angle: _shakeAnimation.value * random,
|
|
child: child!,
|
|
);
|
|
},
|
|
child: child,
|
|
);
|
|
}
|
|
|
|
Widget _draggableWrap({
|
|
required Widget childWhenDragging,
|
|
required Widget feedback,
|
|
required Widget item,
|
|
required int index,
|
|
}) {
|
|
final target = DragTarget<int>(
|
|
builder: (_, __, ___) {
|
|
return AbsorbPointer(
|
|
child: item,
|
|
);
|
|
},
|
|
onWillAcceptWithDetails: (_) {
|
|
debouncer.call(
|
|
DebounceTag.handleWill,
|
|
_handleWill,
|
|
args: [index],
|
|
);
|
|
return false;
|
|
},
|
|
);
|
|
final shakeTarget = ValueListenableBuilder(
|
|
valueListenable: _dragIndexNotifier,
|
|
builder: (_, dragIndex, child) {
|
|
if (dragIndex == index) {
|
|
return child!;
|
|
}
|
|
return _shakeWrap(
|
|
_DeletableContainer(
|
|
onDelete: () {
|
|
_handleDelete(index);
|
|
},
|
|
child: child!,
|
|
),
|
|
);
|
|
},
|
|
child: target,
|
|
);
|
|
final draggableChild = system.isDesktop
|
|
? Draggable(
|
|
childWhenDragging: childWhenDragging,
|
|
data: index,
|
|
feedback: feedback,
|
|
onDragStarted: () {
|
|
_handleDragStarted(index);
|
|
},
|
|
onDragUpdate: (details) {
|
|
_handleDragUpdate(details);
|
|
},
|
|
onDragEnd: (details) {
|
|
_handleDragEnd(details);
|
|
},
|
|
child: shakeTarget,
|
|
)
|
|
: LongPressDraggable(
|
|
childWhenDragging: childWhenDragging,
|
|
data: index,
|
|
feedback: feedback,
|
|
onDragStarted: () {
|
|
_handleDragStarted(index);
|
|
},
|
|
onDragUpdate: (details) {
|
|
_handleDragUpdate(details);
|
|
},
|
|
onDragEnd: (details) {
|
|
_handleDragEnd(details);
|
|
},
|
|
child: shakeTarget,
|
|
);
|
|
return ValueListenableBuilder(
|
|
valueListenable: isEditNotifier,
|
|
builder: (_, isEdit, child) {
|
|
if (!isEdit) {
|
|
return item;
|
|
}
|
|
return child!;
|
|
},
|
|
child: draggableChild,
|
|
);
|
|
}
|
|
|
|
Widget _builderItem(int index) {
|
|
final girdItem = _childrenNotifier.value[index];
|
|
final child = girdItem.child;
|
|
return GridItem(
|
|
mainAxisCellCount: girdItem.mainAxisCellCount,
|
|
crossAxisCellCount: girdItem.crossAxisCellCount,
|
|
child: Builder(
|
|
builder: (context) {
|
|
_itemContexts[index] = context;
|
|
final childWhenDragging = ActivateBox(
|
|
child: Opacity(
|
|
opacity: 0.6,
|
|
child: _sizeBoxWrap(
|
|
CommonCard(
|
|
child: child,
|
|
),
|
|
index,
|
|
),
|
|
),
|
|
);
|
|
final feedback = ActivateBox(
|
|
child: _sizeBoxWrap(
|
|
CommonCard(
|
|
child: Material(
|
|
elevation: 6,
|
|
child: child,
|
|
),
|
|
),
|
|
index,
|
|
),
|
|
);
|
|
return _wrapTransform(
|
|
_draggableWrap(
|
|
childWhenDragging: childWhenDragging,
|
|
feedback: feedback,
|
|
item: child,
|
|
index: index,
|
|
),
|
|
index,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildFakeTransformWidget() {
|
|
return ValueListenableBuilder<bool>(
|
|
valueListenable: _animating,
|
|
builder: (_, animating, __) {
|
|
final index = _dragIndexNotifier.value;
|
|
if (!animating || _fakeDragWidgetAnimation == null || index == -1) {
|
|
return Container();
|
|
}
|
|
return _sizeBoxWrap(
|
|
AnimatedBuilder(
|
|
animation: _fakeDragWidgetAnimation!,
|
|
builder: (_, child) {
|
|
return Transform.translate(
|
|
offset: _fakeDragWidgetAnimation!.value,
|
|
child: child!,
|
|
);
|
|
},
|
|
child: ActivateBox(
|
|
child: _childrenNotifier.value[index].child,
|
|
),
|
|
),
|
|
index,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_scrollable = null;
|
|
_fakeDragWidgetController.dispose();
|
|
_shakeController.dispose();
|
|
_transformController.dispose();
|
|
_dragIndexNotifier.dispose();
|
|
_animating.dispose();
|
|
_childrenNotifier.removeListener(_handleChildrenNotifierChange);
|
|
_childrenNotifier.dispose();
|
|
isEditNotifier.removeListener(_handleIsEditChange);
|
|
isEditNotifier.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DeferredPointerHandler(
|
|
child: Stack(
|
|
children: [
|
|
_ignoreWrap(
|
|
ValueListenableBuilder(
|
|
valueListenable: _childrenNotifier,
|
|
builder: (_, children, __) {
|
|
_onChildrenChange();
|
|
return Grid(
|
|
axisDirection: AxisDirection.down,
|
|
crossAxisCount: crossCount,
|
|
crossAxisSpacing: widget.crossAxisSpacing,
|
|
mainAxisSpacing: widget.mainAxisSpacing,
|
|
children: [
|
|
for (int i = 0; i < children.length; i++) _builderItem(i),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
_buildFakeTransformWidget(),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddedWidgetsModal extends StatelessWidget {
|
|
final List<GridItem> items;
|
|
final Function(GridItem item) onAdd;
|
|
|
|
const _AddedWidgetsModal({
|
|
required this.items,
|
|
required this.onAdd,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return DeferredPointerHandler(
|
|
child: SingleChildScrollView(
|
|
padding: EdgeInsets.all(
|
|
16,
|
|
),
|
|
child: Grid(
|
|
crossAxisCount: 8,
|
|
crossAxisSpacing: 16,
|
|
mainAxisSpacing: 16,
|
|
children: items
|
|
.map(
|
|
(item) => item.wrap(
|
|
builder: (child) {
|
|
return _AddedContainer(
|
|
onAdd: () {
|
|
onAdd(item);
|
|
},
|
|
child: child,
|
|
);
|
|
},
|
|
),
|
|
)
|
|
.toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _DeletableContainer extends StatefulWidget {
|
|
final Widget child;
|
|
final VoidCallback onDelete;
|
|
|
|
const _DeletableContainer({
|
|
required this.child,
|
|
required this.onDelete,
|
|
});
|
|
|
|
@override
|
|
State<_DeletableContainer> createState() => _DeletableContainerState();
|
|
}
|
|
|
|
class _DeletableContainerState extends State<_DeletableContainer>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _scaleAnimation;
|
|
late Animation<double> _fadeAnimation;
|
|
bool _deleteButtonVisible = true;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: commonDuration,
|
|
);
|
|
_scaleAnimation = Tween(begin: 1.0, end: 0.4).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.easeIn,
|
|
),
|
|
);
|
|
_fadeAnimation = Tween(begin: 1.0, end: 0.0).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.easeIn,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_DeletableContainer oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.child != widget.child) {
|
|
setState(() {
|
|
_controller.value = 0;
|
|
_deleteButtonVisible = true;
|
|
});
|
|
}
|
|
}
|
|
|
|
_handleDel() async {
|
|
setState(() {
|
|
_deleteButtonVisible = false;
|
|
});
|
|
await _controller.forward(from: 0);
|
|
widget.onDelete();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
AnimatedBuilder(
|
|
animation: _controller.view,
|
|
builder: (_, child) {
|
|
return Transform.scale(
|
|
scale: _scaleAnimation.value,
|
|
child: Opacity(
|
|
opacity: _fadeAnimation.value,
|
|
child: child!,
|
|
),
|
|
);
|
|
},
|
|
child: widget.child,
|
|
),
|
|
if (_deleteButtonVisible)
|
|
Positioned(
|
|
top: -8,
|
|
right: -8,
|
|
child: DeferPointer(
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: IconButton.filled(
|
|
iconSize: 20,
|
|
padding: EdgeInsets.all(2),
|
|
onPressed: _handleDel,
|
|
icon: Icon(
|
|
Icons.close,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _AddedContainer extends StatefulWidget {
|
|
final Widget child;
|
|
final VoidCallback onAdd;
|
|
|
|
const _AddedContainer({
|
|
required this.child,
|
|
required this.onAdd,
|
|
});
|
|
|
|
@override
|
|
State<_AddedContainer> createState() => _AddedContainerState();
|
|
}
|
|
|
|
class _AddedContainerState extends State<_AddedContainer> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(_AddedContainer oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.child != widget.child) {}
|
|
}
|
|
|
|
_handleAdd() async {
|
|
widget.onAdd();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
ActivateBox(
|
|
child: widget.child,
|
|
),
|
|
Positioned(
|
|
top: -8,
|
|
right: -8,
|
|
child: DeferPointer(
|
|
child: SizedBox(
|
|
width: 24,
|
|
height: 24,
|
|
child: IconButton.filled(
|
|
iconSize: 20,
|
|
padding: EdgeInsets.all(2),
|
|
onPressed: _handleAdd,
|
|
icon: Icon(
|
|
Icons.add,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
)
|
|
],
|
|
);
|
|
}
|
|
}
|