Files
MWClash/lib/widgets/super_grid.dart
chen08209 676f2d058a Add windows server mode start process verify
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
2025-05-01 00:02:29 +08:00

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