Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-07-30 13:53:37 +08:00
parent b5b15dbed6
commit c4c0852dea
105 changed files with 2063 additions and 1360 deletions

View File

@@ -1,4 +1,3 @@
import 'package:flutter/gestures.dart' show Drag;
import 'package:flutter/material.dart';
@@ -25,7 +24,9 @@ class DynDraggableScrollableSheet extends DraggableScrollableSheet {
class _DynDraggableScrollableSheetState extends DraggableScrollableSheetState {
@override
void initScrollController() {
scrollController = _DynDraggableScrollableSheetScrollController(extent: extent);
scrollController = _DynDraggableScrollableSheetScrollController(
extent: extent,
);
}
}

View File

@@ -22,10 +22,13 @@ import 'package:PiliPlus/common/style.dart';
import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/image_grid/image_grid_builder.dart';
import 'package:PiliPlus/common/widgets/image_viewer/gallery_viewer.dart';
import 'package:PiliPlus/common/widgets/scaffold/mini_scaffold.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart';
import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/size_ext.dart';
import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
@@ -94,13 +97,17 @@ class ImageGridView extends StatelessWidget {
!fullScreen &&
Get.currentRoute.startsWith(_regex) &&
!context.mediaQuerySize.isPortrait) {
final scaffoldState = Scaffold.maybeOf(context);
final scaffoldState = MiniScaffold.maybeOf(context);
if (scaffoldState != null) {
onViewImage?.call();
PageUtils.onHorizontalPreviewState(
scaffoldState,
imgList,
index,
scaffoldState.showBottomSheet(
constraints: const BoxConstraints(),
(context) => GalleryViewer(
sources: imgList,
initIndex: index,
quality: GlobalData().imgQuality,
),
enableDrag: false,
);
return;
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
show BoxParentData, BoxHitTestResult, ChildLayoutHelper;
enum MainType { sideBar, body, bottomNav }
enum MainType { sideBar, bottomNav, body }
class MainLayout
extends SlottedMultiChildRenderObjectWidget<MainType, RenderBox> {
@@ -37,6 +37,10 @@ class MainLayout
class _RenderMainLayout extends RenderBox
with SlottedContainerRenderObjectMixin<MainType, RenderBox> {
RenderBox? get sideBar => childForSlot(.sideBar);
RenderBox? get bottomNav => childForSlot(.bottomNav);
RenderBox get body => childForSlot(.body)!;
Offset _getOffset(RenderBox child) {
return (child.parentData as BoxParentData).offset;
}
@@ -53,7 +57,7 @@ class _RenderMainLayout extends RenderBox
final Offset bodyOffset;
final BoxConstraints bodyConstraints;
final sideBar = childForSlot(.sideBar);
final sideBar = this.sideBar;
if (sideBar != null) {
final sideBarWidth = ChildLayoutHelper.layoutChild(
sideBar,
@@ -67,7 +71,7 @@ class _RenderMainLayout extends RenderBox
height: constraints.maxHeight,
);
} else {
final bottomNav = childForSlot(.bottomNav);
final bottomNav = this.bottomNav;
if (bottomNav != null) {
final bottomNavSize = ChildLayoutHelper.layoutChild(
bottomNav,
@@ -89,20 +93,26 @@ class _RenderMainLayout extends RenderBox
);
}
final body = childForSlot(.body)!..layout(bodyConstraints);
final body = this.body..layout(bodyConstraints);
_setOffset(body, bodyOffset);
}
@override
void paint(PaintingContext context, Offset offset) {
for (final child in children) {
context.paintChild(child, _getOffset(child) + offset);
void doPaint(RenderBox? child) {
if (child != null) {
context.paintChild(child, _getOffset(child) + offset);
}
}
doPaint(sideBar);
doPaint(body);
doPaint(bottomNav);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
for (final type in MainType.values.reversed) {
for (final type in MainType.values) {
final child = childForSlot(type);
if (child == null) continue;
final bool isHit = result.addWithPaintOffset(

View File

@@ -0,0 +1,80 @@
import 'package:flutter/gestures.dart' show VerticalDragGestureRecognizer;
import 'package:flutter/material.dart';
// ignore: camel_case_types
class BottomSheet_ extends BottomSheet {
const BottomSheet_({
super.key,
super.animationController,
super.enableDrag = true,
super.onDragStart,
super.onDragEnd,
super.constraints,
required super.onClosing,
required super.builder,
});
@override
BottomSheetState createState() => _MiniBottomSheetState();
}
class _MiniBottomSheetState extends BottomSheetState {
VerticalDragGestureRecognizer? _verticalDragGestureRecognizer;
VerticalDragGestureRecognizer get verticalDragGestureRecognizer =>
_verticalDragGestureRecognizer ??=
VerticalDragGestureRecognizer(debugOwner: this)
..onStart = handleDragStart
..onUpdate = handleDragUpdate
..onEnd = handleDragEnd
..gestureSettings = MediaQuery.maybeGestureSettingsOf(context)
..onlyAcceptDragOnThreshold = true;
@override
void dispose() {
super.dispose();
if (widget.enableDrag) {
_verticalDragGestureRecognizer?.dispose();
_verticalDragGestureRecognizer = null;
}
}
void _onPointerDown(PointerDownEvent event) {
verticalDragGestureRecognizer.addPointer(event);
}
@override
Widget build(BuildContext context) {
final bool useMaterial3 = Theme.of(context).useMaterial3;
late final defaults = useMaterial3
? BottomSheetDefaultsM3(context)
: const BottomSheetThemeData();
late final bottomSheetTheme = Theme.of(context).bottomSheetTheme;
final BoxConstraints? constraints =
widget.constraints ??
bottomSheetTheme.constraints ??
defaults.constraints;
Widget bottomSheet = KeyedSubtree(
key: childKey,
child: widget.builder(context),
);
if (constraints != null && constraints != const BoxConstraints()) {
bottomSheet = Align(
alignment: Alignment.bottomCenter,
heightFactor: 1.0,
child: ConstrainedBox(constraints: constraints, child: bottomSheet),
);
}
if (widget.enableDrag) {
return Listener(
onPointerDown: _onPointerDown,
child: bottomSheet,
);
}
return bottomSheet;
}
}

View File

@@ -0,0 +1,308 @@
import 'dart:async' show Completer;
import 'package:PiliPlus/common/widgets/scaffold/bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
show RenderStack, BoxHitTestResult, StackParentData;
class MiniScaffold extends StatefulWidget {
const MiniScaffold({
super.key,
required this.body,
});
final Widget body;
static MiniScaffoldState of(BuildContext context) {
return context.findAncestorStateOfType<MiniScaffoldState>()!;
}
static MiniScaffoldState? maybeOf(BuildContext context) {
return context.findAncestorStateOfType<MiniScaffoldState>();
}
@override
State<MiniScaffold> createState() => MiniScaffoldState();
}
class MiniScaffoldState extends State<MiniScaffold>
with TickerProviderStateMixin {
final _dismissedBottomSheets = <StandardBottomSheet>[];
PersistentBottomSheetController? _currentBottomSheet;
LocalHistoryEntry? _persistentSheetHistoryEntry;
void _closeCurrentBottomSheet() {
if (_currentBottomSheet != null) {
if (!_currentBottomSheet!.isLocalHistoryEntry) {
_currentBottomSheet!.close();
}
assert(() {
_currentBottomSheet?.completer.future.whenComplete(() {
assert(_currentBottomSheet == null);
});
return true;
}());
}
}
PersistentBottomSheetController _buildBottomSheet(
WidgetBuilder builder, {
required bool isPersistent,
required AnimationController animationController,
BoxConstraints? constraints,
bool? enableDrag,
bool shouldDisposeAnimationController = true,
}) {
final completer = Completer<void>();
final bottomSheetKey = GlobalKey<StandardBottomSheetState>();
late StandardBottomSheet bottomSheet;
var removedEntry = false;
var doingDispose = false;
void removePersistentSheetHistoryEntryIfNeeded() {
assert(isPersistent);
if (_persistentSheetHistoryEntry != null) {
_persistentSheetHistoryEntry!.remove();
_persistentSheetHistoryEntry = null;
}
}
void removeCurrentBottomSheet() {
removedEntry = true;
if (_currentBottomSheet == null) {
return;
}
assert(_currentBottomSheet!.widget == bottomSheet);
assert(bottomSheetKey.currentState != null);
if (isPersistent) {
removePersistentSheetHistoryEntryIfNeeded();
}
bottomSheetKey.currentState!.close();
setState(() {
_currentBottomSheet = null;
});
if (!animationController.isDismissed) {
_dismissedBottomSheets.add(bottomSheet);
}
completer.complete();
}
final LocalHistoryEntry? entry = isPersistent
? null
: LocalHistoryEntry(
onRemove: () {
if (!removedEntry &&
_currentBottomSheet?.widget == bottomSheet &&
!doingDispose) {
removeCurrentBottomSheet();
}
},
);
void removeEntryIfNeeded() {
if (!isPersistent && !removedEntry) {
assert(entry != null);
entry!.remove();
removedEntry = true;
}
}
bottomSheet = _StandardBottomSheet(
key: bottomSheetKey,
animationController: animationController,
enableDrag: enableDrag ?? !isPersistent,
onClosing: () {
if (_currentBottomSheet == null) {
return;
}
assert(_currentBottomSheet!.widget == bottomSheet);
removeEntryIfNeeded();
},
onDismissed: () {
if (_dismissedBottomSheets.contains(bottomSheet)) {
setState(() {
_dismissedBottomSheets.remove(bottomSheet);
});
}
},
onDispose: () {
doingDispose = true;
removeEntryIfNeeded();
if (shouldDisposeAnimationController) {
animationController.dispose();
}
},
builder: builder,
isPersistent: isPersistent,
constraints: constraints,
);
if (!isPersistent) {
ModalRoute.of(context)!.addLocalHistoryEntry(entry!);
}
return PersistentBottomSheetController(
bottomSheet,
completer,
entry != null ? entry.remove : removeCurrentBottomSheet,
(VoidCallback fn) {
bottomSheetKey.currentState?.setState(fn);
},
!isPersistent,
);
}
PersistentBottomSheetController showBottomSheet(
WidgetBuilder builder, {
BoxConstraints? constraints,
bool? enableDrag,
AnimationController? transitionAnimationController,
AnimationStyle? sheetAnimationStyle,
}) {
assert(debugCheckHasMediaQuery(context));
_closeCurrentBottomSheet();
final AnimationController controller =
(transitionAnimationController ??
BottomSheet.createAnimationController(
this,
sheetAnimationStyle: sheetAnimationStyle,
))
..forward();
setState(() {
_currentBottomSheet = _buildBottomSheet(
builder,
isPersistent: false,
animationController: controller,
constraints: constraints,
enableDrag: enableDrag,
shouldDisposeAnimationController: transitionAnimationController == null,
);
});
return _currentBottomSheet!;
}
@override
Widget build(BuildContext context) {
return BottomSheetStack(
clipBehavior: .none,
alignment: .bottomCenter,
children: [
widget.body,
..._dismissedBottomSheets,
?_currentBottomSheet?.widget,
],
);
}
}
class _StandardBottomSheet extends StandardBottomSheet {
const _StandardBottomSheet({
super.key,
required super.animationController,
super.enableDrag,
required super.onClosing,
required super.onDismissed,
required super.builder,
super.isPersistent,
super.constraints,
super.onDispose,
});
@override
StandardBottomSheetState createState() => _StandardBottomSheetState();
}
class _StandardBottomSheetState extends StandardBottomSheetState {
@override
Widget build(BuildContext context) {
final child = BottomSheet_(
animationController: widget.animationController,
enableDrag: widget.enableDrag,
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
onClosing: widget.onClosing!,
builder: widget.builder,
constraints: widget.constraints,
);
if (widget.enableDrag) {
return AnimatedBuilder(
animation: widget.animationController,
builder: (context, child) => Align(
alignment: AlignmentDirectional.topStart,
heightFactor: animationCurve.transform(
widget.animationController.value,
),
child: child,
),
child: child,
);
}
return AnimatedBuilder(
animation: widget.animationController,
builder: (context, child) => Opacity(
opacity: widget.animationController.value,
child: child,
),
child: child,
);
}
}
class BottomSheetStack extends Stack {
const BottomSheetStack({
super.key,
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
super.children,
});
@override
RenderBottomSheetStack createRenderObject(BuildContext context) {
return RenderBottomSheetStack(
alignment: alignment,
textDirection: textDirection ?? Directionality.maybeOf(context),
fit: fit,
clipBehavior: clipBehavior,
);
}
}
class RenderBottomSheetStack extends RenderStack {
RenderBottomSheetStack({
super.children,
super.alignment,
super.textDirection,
super.fit,
super.clipBehavior,
});
/// HitTest lastChild only
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
RenderBox? child = lastChild;
if (child != null) {
final childParentData = child.parentData! as StackParentData;
return result.addWithPaintOffset(
offset: childParentData.offset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
assert(transformed == position - childParentData.offset);
final isHit = child.hitTest(result, position: transformed);
if (childParentData.previousSibling != null) {
return false;
} else {
return isHit;
}
},
);
}
return false;
}
}

View File

@@ -0,0 +1,152 @@
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'
show BoxParentData, BoxHitTestResult, ChildLayoutHelper;
class SimpleScaffold extends StatelessWidget {
const SimpleScaffold({
super.key,
this.backgroundColor,
this.fab,
this.appBar,
required this.body,
});
final Color? backgroundColor;
final Widget? fab;
final Widget? appBar;
final Widget body;
@override
Widget build(BuildContext context) {
return Material(
color: backgroundColor,
child: ScaffoldLayout(
fab: fab,
appBar: appBar,
body: body,
),
);
}
}
enum ScaffoldType { fab, appBar, body }
class ScaffoldLayout
extends SlottedMultiChildRenderObjectWidget<ScaffoldType, RenderBox> {
const ScaffoldLayout({
super.key,
this.fab,
this.appBar,
required this.body,
});
final Widget? fab;
final Widget? appBar;
final Widget body;
@override
Iterable<ScaffoldType> get slots => ScaffoldType.values;
@override
Widget? childForSlot(slot) => switch (slot) {
.fab => fab,
.appBar => appBar,
.body => body,
};
@override
SlottedContainerRenderObjectMixin<ScaffoldType, RenderBox> createRenderObject(
BuildContext context,
) {
return _RenderScaffoldLayout();
}
}
class _RenderScaffoldLayout extends RenderBox
with SlottedContainerRenderObjectMixin<ScaffoldType, RenderBox> {
RenderBox? get fab => childForSlot(.fab);
RenderBox? get appBar => childForSlot(.appBar);
RenderBox get body => childForSlot(.body)!;
Offset _getOffset(RenderBox child) {
return (child.parentData as BoxParentData).offset;
}
void _setOffset(RenderBox child, Offset offset) {
(child.parentData as BoxParentData).offset = offset;
}
@override
void performLayout() {
final constraints = this.constraints;
size = constraints.biggest;
final Offset bodyOffset;
final BoxConstraints bodyConstraints;
final appBar = this.appBar;
if (appBar != null) {
final appBarHeight = ChildLayoutHelper.layoutChild(
appBar,
BoxConstraints.tightFor(width: constraints.maxWidth),
).height;
_setOffset(appBar, .zero);
bodyOffset = Offset(0, appBarHeight);
bodyConstraints = BoxConstraints.tightFor(
width: constraints.maxWidth,
height: constraints.maxHeight - appBarHeight,
);
} else {
bodyOffset = .zero;
bodyConstraints = constraints;
}
final body = this.body..layout(bodyConstraints);
_setOffset(body, bodyOffset);
final fab = this.fab;
if (fab != null) {
final fabSize = ChildLayoutHelper.layoutChild(fab, constraints.loosen());
_setOffset(
fab,
Offset(
constraints.maxWidth - fabSize.width,
constraints.maxHeight - fabSize.height,
),
);
}
}
@override
void paint(PaintingContext context, Offset offset) {
void doPaint(RenderBox? child) {
if (child != null) {
context.paintChild(child, _getOffset(child) + offset);
}
}
doPaint(appBar);
doPaint(body);
doPaint(fab);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
for (final type in ScaffoldType.values) {
final child = childForSlot(type);
if (child == null) continue;
final bool isHit = result.addWithPaintOffset(
offset: _getOffset(child),
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) {
return child.hitTest(result, position: transformed);
},
);
if (isHit) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,68 @@
import 'package:flutter/rendering.dart' show RenderProxyBox;
import 'package:flutter/widgets.dart';
class SimpleColoredBox extends ColoredBox {
const SimpleColoredBox({
required super.color,
super.isAntiAlias,
super.child,
super.key,
});
@override
RenderObject createRenderObject(BuildContext context) {
return RenderSimpleColoredBox(color: color, isAntiAlias: isAntiAlias);
}
@override
void updateRenderObject(
BuildContext context,
RenderSimpleColoredBox renderObject,
) {
renderObject
..color = color
..isAntiAlias = isAntiAlias;
}
}
class RenderSimpleColoredBox extends RenderProxyBox {
RenderSimpleColoredBox({
required this._color,
required this._isAntiAlias,
});
Color get color => _color;
Color _color;
set color(Color value) {
if (value == _color) {
return;
}
_color = value;
markNeedsPaint();
}
bool get isAntiAlias => _isAntiAlias;
bool _isAntiAlias;
set isAntiAlias(bool value) {
if (value == _isAntiAlias) {
return;
}
_isAntiAlias = value;
markNeedsPaint();
}
@override
void paint(PaintingContext context, Offset offset) {
if (size > Size.zero) {
context.canvas.drawRect(
offset & size,
Paint()
..isAntiAlias = isAntiAlias
..color = color,
);
}
if (child != null) {
context.paintChild(child!, offset);
}
}
}