diff --git a/lib/common/widgets/image/custom_grid_view.dart b/lib/common/widgets/image/custom_grid_view.dart
deleted file mode 100644
index 0791e71b0..000000000
--- a/lib/common/widgets/image/custom_grid_view.dart
+++ /dev/null
@@ -1,544 +0,0 @@
-/*
- * This file is part of PiliPlus
- *
- * PiliPlus is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * PiliPlus is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with PiliPlus. If not, see .
- */
-
-import 'dart:io' show Platform;
-import 'dart:math' show min;
-
-import 'package:PiliPlus/common/constants.dart';
-import 'package:PiliPlus/common/widgets/badge.dart';
-import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
-import 'package:PiliPlus/models/common/badge_type.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/image_utils.dart';
-import 'package:PiliPlus/utils/page_utils.dart';
-import 'package:PiliPlus/utils/platform_utils.dart';
-import 'package:PiliPlus/utils/storage_pref.dart';
-import 'package:flutter/gestures.dart'
- show TapGestureRecognizer, LongPressGestureRecognizer;
-import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart'
- show
- ContainerRenderObjectMixin,
- RenderBoxContainerDefaultsMixin,
- MultiChildLayoutParentData,
- BoxHitTestResult,
- BoxHitTestEntry;
-import 'package:flutter/services.dart' show HapticFeedback;
-import 'package:get/get_core/src/get_main.dart';
-import 'package:get/get_navigation/get_navigation.dart';
-
-class ImageModel {
- ImageModel({
- required num? width,
- required num? height,
- required this.url,
- this.liveUrl,
- }) {
- this.width = width == null || width == 0 ? 1 : width;
- this.height = height == null || height == 0 ? 1 : height;
- }
-
- late num width;
- late num height;
- String url;
- String? liveUrl;
- bool? _isLongPic;
- bool? _isLivePhoto;
-
- bool get isLongPic =>
- _isLongPic ??= (height / width) > StyleString.imgMaxRatio;
- bool get isLivePhoto =>
- _isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
-
- static bool enableLivePhoto = Pref.enableLivePhoto;
-}
-
-class CustomGridView extends StatelessWidget {
- const CustomGridView({
- super.key,
- this.space = 5,
- required this.maxWidth,
- required this.picArr,
- this.onViewImage,
- this.fullScreen = false,
- });
-
- final double maxWidth;
- final double space;
- final List picArr;
- final VoidCallback? onViewImage;
- final bool fullScreen;
-
- static bool horizontalPreview = Pref.horizontalPreview;
- static const _routes = ['/videoV', '/dynamicDetail'];
-
- void onTap(BuildContext context, int index) {
- final imgList = picArr.map(
- (item) {
- bool isLive = item.isLivePhoto;
- return SourceModel(
- sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage,
- url: item.url,
- liveUrl: isLive ? item.liveUrl : null,
- width: isLive ? item.width.toInt() : null,
- height: isLive ? item.height.toInt() : null,
- isLongPic: item.isLongPic,
- );
- },
- ).toList();
- if (horizontalPreview &&
- !fullScreen &&
- _routes.contains(Get.currentRoute) &&
- !context.mediaQuerySize.isPortrait) {
- final scaffoldState = Scaffold.maybeOf(context);
- if (scaffoldState != null) {
- onViewImage?.call();
- PageUtils.onHorizontalPreviewState(
- scaffoldState,
- imgList,
- index,
- );
- return;
- }
- }
- PageUtils.imageView(
- initialPage: index,
- imgList: imgList,
- );
- }
-
- static BorderRadius _borderRadius(
- int col,
- int length,
- int index, {
- Radius r = StyleString.imgRadius,
- }) {
- if (length == 1) return StyleString.mdRadius;
-
- final bool hasUp = index - col >= 0;
- final bool hasDown = index + col < length;
-
- final bool isRowStart = (index % col) == 0;
- final bool isRowEnd = (index % col) == col - 1 || index == length - 1;
-
- final bool hasLeft = !isRowStart;
- final bool hasRight = !isRowEnd && (index + 1) < length;
-
- return BorderRadius.only(
- topLeft: !hasUp && !hasLeft ? r : Radius.zero,
- topRight: !hasUp && !hasRight ? r : Radius.zero,
- bottomLeft: !hasDown && !hasLeft ? r : Radius.zero,
- bottomRight: !hasDown && !hasRight ? r : Radius.zero,
- );
- }
-
- static bool enableImgMenu = Pref.enableImgMenu;
-
- void _showMenu(BuildContext context, int index, Offset offset) {
- HapticFeedback.mediumImpact();
- final item = picArr[index];
- showMenu(
- context: context,
- position: PageUtils.menuPosition(offset),
- items: [
- if (PlatformUtils.isMobile)
- PopupMenuItem(
- height: 42,
- onTap: () => ImageUtils.onShareImg(item.url),
- child: const Text('分享', style: TextStyle(fontSize: 14)),
- ),
- PopupMenuItem(
- height: 42,
- onTap: () => ImageUtils.downloadImg([item.url]),
- child: const Text('保存图片', style: TextStyle(fontSize: 14)),
- ),
- if (PlatformUtils.isDesktop)
- PopupMenuItem(
- height: 42,
- onTap: () => PageUtils.launchURL(item.url),
- child: const Text('网页打开', style: TextStyle(fontSize: 14)),
- )
- else if (picArr.length > 1)
- PopupMenuItem(
- height: 42,
- onTap: () =>
- ImageUtils.downloadImg(picArr.map((item) => item.url).toList()),
- child: const Text('保存全部', style: TextStyle(fontSize: 14)),
- ),
- if (item.isLivePhoto)
- PopupMenuItem(
- height: 42,
- onTap: () => ImageUtils.downloadLivePhoto(
- url: item.url,
- liveUrl: item.liveUrl!,
- width: item.width.toInt(),
- height: item.height.toInt(),
- ),
- child: Text(
- '保存${Platform.isIOS ? '实况' : '视频'}',
- style: const TextStyle(fontSize: 14),
- ),
- ),
- ],
- );
- }
-
- @override
- Widget build(BuildContext context) {
- double imageWidth;
- double imageHeight;
- final length = picArr.length;
- final isSingle = length == 1;
- final isFour = length == 4;
- if (length == 2) {
- imageWidth = imageHeight = (maxWidth - space) / 2;
- } else {
- imageHeight = imageWidth = (maxWidth - 2 * space) / 3;
- if (isSingle) {
- final img = picArr.first;
- final width = img.width;
- final height = img.height;
- final ratioWH = width / height;
- final ratioHW = height / width;
- imageWidth = ratioWH > 1.5
- ? maxWidth
- : (ratioWH >= 1 || (height > width && ratioHW < 1.5))
- ? 2 * imageWidth
- : 1.5 * imageWidth;
- if (width != 1) {
- imageWidth = min(imageWidth, width.toDouble());
- }
- imageHeight = imageWidth * min(ratioHW, StyleString.imgMaxRatio);
- }
- }
-
- final int column = isFour ? 2 : 3;
- final int row = isFour ? 2 : (length / 3).ceil();
- late final placeHolder = Container(
- width: imageWidth,
- height: imageHeight,
- decoration: BoxDecoration(
- color: Theme.of(
- context,
- ).colorScheme.onInverseSurface.withValues(alpha: 0.4),
- ),
- child: Image.asset(
- 'assets/images/loading.png',
- width: imageWidth,
- height: imageHeight,
- cacheWidth: imageWidth.cacheSize(context),
- ),
- );
-
- return Padding(
- padding: const EdgeInsets.only(top: 6),
- child: SizedBox(
- width: maxWidth,
- height: imageHeight * row + space * (row - 1),
- child: ImageGrid(
- space: space,
- column: column,
- width: imageWidth,
- height: imageHeight,
- onTap: (index) => onTap(context, index),
- onSecondaryTapUp: enableImgMenu && PlatformUtils.isDesktop
- ? (index, offset) => _showMenu(context, index, offset)
- : null,
- onLongPressStart: enableImgMenu && PlatformUtils.isMobile
- ? (index, offset) => _showMenu(context, index, offset)
- : null,
- children: List.generate(length, (index) {
- final item = picArr[index];
- final borderRadius = _borderRadius(column, length, index);
- Widget child = Stack(
- clipBehavior: Clip.none,
- alignment: Alignment.center,
- children: [
- NetworkImgLayer(
- src: item.url,
- width: imageWidth,
- height: imageHeight,
- borderRadius: borderRadius,
- alignment: item.isLongPic ? .topCenter : .center,
- cacheWidth: item.width <= item.height,
- getPlaceHolder: () => placeHolder,
- ),
- if (item.isLivePhoto)
- const PBadge(
- text: 'Live',
- right: 8,
- bottom: 8,
- type: PBadgeType.gray,
- )
- else if (item.isLongPic)
- const PBadge(
- text: '长图',
- right: 8,
- bottom: 8,
- ),
- ],
- );
- if (!item.isLongPic) {
- child = Hero(
- tag: item.url,
- child: child,
- );
- }
- return LayoutId(
- id: index,
- child: child,
- );
- }),
- ),
- ),
- );
- }
-}
-
-class ImageGrid extends MultiChildRenderObjectWidget {
- const ImageGrid({
- super.key,
- super.children,
- required this.space,
- required this.column,
- required this.width,
- required this.height,
- required this.onTap,
- required this.onSecondaryTapUp,
- required this.onLongPressStart,
- });
-
- final double space;
- final int column;
- final double width;
- final double height;
- final ValueChanged onTap;
- final OnShowMenu? onSecondaryTapUp;
- final OnShowMenu? onLongPressStart;
-
- @override
- RenderObject createRenderObject(BuildContext context) {
- return RenderImageGrid(
- space: space,
- column: column,
- width: width,
- height: height,
- onTap: onTap,
- onSecondaryTapUp: onSecondaryTapUp,
- onLongPressStart: onLongPressStart,
- );
- }
-
- @override
- void updateRenderObject(BuildContext context, RenderImageGrid renderObject) {
- renderObject
- ..space = space
- ..column = column
- ..width = width
- ..height = height
- ..onTap = onTap
- ..onSecondaryTapUp = onSecondaryTapUp
- ..onLongPressStart = onLongPressStart;
- }
-}
-
-typedef OnShowMenu = Function(int index, Offset offset);
-
-class RenderImageGrid extends RenderBox
- with
- ContainerRenderObjectMixin,
- RenderBoxContainerDefaultsMixin {
- RenderImageGrid({
- required double space,
- required int column,
- required double width,
- required double height,
- required ValueChanged onTap,
- required OnShowMenu? onSecondaryTapUp,
- required OnShowMenu? onLongPressStart,
- }) : _space = space,
- _column = column,
- _width = width,
- _height = height,
- _onTap = onTap,
- _onSecondaryTapUp = onSecondaryTapUp,
- _onLongPressStart = onLongPressStart {
- _tapGestureRecognizer = TapGestureRecognizer()..onTap = _handleOnTap;
- if (onSecondaryTapUp != null) {
- _tapGestureRecognizer.onSecondaryTapUp = _handleSecondaryTapUp;
- }
- if (onLongPressStart != null) {
- _longPressGestureRecognizer = LongPressGestureRecognizer()
- ..onLongPressStart = _handleLongPressStart;
- }
- }
-
- ValueChanged _onTap;
- set onTap(ValueChanged value) {
- _onTap = value;
- }
-
- OnShowMenu? _onSecondaryTapUp;
- set onSecondaryTapUp(OnShowMenu? value) {
- _onSecondaryTapUp = value;
- }
-
- OnShowMenu? _onLongPressStart;
- set onLongPressStart(OnShowMenu? value) {
- _onLongPressStart = value;
- }
-
- int? _index;
-
- void _handleOnTap() {
- _onTap(_index!);
- }
-
- void _handleSecondaryTapUp(TapUpDetails details) {
- _onSecondaryTapUp!(_index!, details.globalPosition);
- }
-
- void _handleLongPressStart(LongPressStartDetails details) {
- _onLongPressStart!(_index!, details.globalPosition);
- }
-
- double _space;
- double get space => _space;
- set space(double value) {
- if (_space == value) return;
- _space = value;
- markNeedsLayout();
- }
-
- int _column;
- int get column => _column;
- set column(int value) {
- if (_column == value) return;
- _column = value;
- markNeedsLayout();
- }
-
- double _width;
- double get width => _width;
- set width(double value) {
- if (_width == value) return;
- _width = value;
- markNeedsLayout();
- }
-
- double _height;
- double get height => _height;
- set height(double value) {
- if (_height == value) return;
- _height = value;
- markNeedsLayout();
- }
-
- @override
- void setupParentData(RenderBox child) {
- if (child.parentData is! MultiChildLayoutParentData) {
- child.parentData = MultiChildLayoutParentData();
- }
- }
-
- @override
- void performLayout() {
- size = constraints.constrain(constraints.biggest);
-
- final itemConstraints = BoxConstraints(
- minWidth: width,
- maxWidth: width,
- minHeight: height,
- maxHeight: height,
- );
- RenderBox? child = firstChild;
- while (child != null) {
- final childParentData = child.parentData as MultiChildLayoutParentData;
- final index = childParentData.id as int;
- child.layout(itemConstraints);
- childParentData.offset = Offset(
- (space + width) * (index % column),
- (space + height) * (index ~/ column),
- );
- child = childParentData.nextSibling;
- }
- }
-
- @override
- void paint(PaintingContext context, Offset offset) {
- defaultPaint(context, offset);
- }
-
- @override
- bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
- RenderBox? child = lastChild;
- while (child != null) {
- final childParentData = child.parentData as MultiChildLayoutParentData;
- final bool isHit = result.addWithPaintOffset(
- offset: childParentData.offset,
- position: position,
- hitTest: (BoxHitTestResult result, Offset transformed) {
- assert(transformed == position - childParentData.offset);
- if (child!.size.contains(transformed)) {
- result.add(BoxHitTestEntry(child, transformed));
- return true;
- }
- return false;
- },
- );
- if (isHit) {
- _index = childParentData.id as int;
- return true;
- }
- child = childParentData.previousSibling;
- }
- _index = null;
- return false;
- }
-
- @override
- void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
- if (event is PointerDownEvent) {
- _tapGestureRecognizer.addPointer(event);
- _longPressGestureRecognizer?.addPointer(event);
- }
- }
-
- late final TapGestureRecognizer _tapGestureRecognizer;
- LongPressGestureRecognizer? _longPressGestureRecognizer;
-
- @override
- void dispose() {
- _tapGestureRecognizer
- ..onTap = null
- ..onSecondaryTapUp = null
- ..dispose();
- _longPressGestureRecognizer
- ?..onLongPressStart = null
- ..dispose();
- _longPressGestureRecognizer = null;
- _onSecondaryTapUp = null;
- _onLongPressStart = null;
- super.dispose();
- }
-
- @override
- bool get isRepaintBoundary => true; // gif repaint
-}
diff --git a/lib/common/widgets/image_grid/image_grid_builder.dart b/lib/common/widgets/image_grid/image_grid_builder.dart
new file mode 100644
index 000000000..3203c89b9
--- /dev/null
+++ b/lib/common/widgets/image_grid/image_grid_builder.dart
@@ -0,0 +1,634 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:collection' show HashSet;
+import 'dart:math' as math;
+
+import 'package:PiliPlus/common/constants.dart' show StyleString;
+import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart'
+ show ImageModel;
+import 'package:flutter/foundation.dart' show kDebugMode;
+import 'package:flutter/gestures.dart'
+ show TapGestureRecognizer, LongPressGestureRecognizer;
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart'
+ show
+ ContainerRenderObjectMixin,
+ MultiChildLayoutParentData,
+ RenderBoxContainerDefaultsMixin,
+ RenderObjectWithLayoutCallbackMixin,
+ Constraints,
+ LayoutCallback,
+ BoxHitTestResult,
+ BoxHitTestEntry,
+ ContainerParentDataMixin,
+ InformationCollector,
+ DiagnosticsDebugCreator;
+import 'package:flutter/scheduler.dart';
+
+/// ref [LayoutBuilder]
+
+const space = 5.0;
+typedef ImageGridInfo = ({int column, int row, Size size});
+
+class ImageGridBuilder extends RenderObjectWidget {
+ const ImageGridBuilder({
+ super.key,
+ required this.picArr,
+ required this.onTap,
+ required this.onSecondaryTapUp,
+ required this.onLongPressStart,
+ required this.builder,
+ });
+
+ final List picArr;
+ final ValueChanged onTap;
+ final OnShowMenu? onSecondaryTapUp;
+ final OnShowMenu? onLongPressStart;
+ final List Function(BuildContext context, ImageGridInfo imageGridInfo)
+ builder;
+
+ @protected
+ bool updateShouldRebuild(ImageGridBuilder oldWidget) => true;
+
+ @override
+ ImageGridRenderObjectElement createElement() =>
+ ImageGridRenderObjectElement(this);
+
+ @override
+ RenderObject createRenderObject(BuildContext context) {
+ return RenderImageGrid(
+ onTap: onTap,
+ onSecondaryTapUp: onSecondaryTapUp,
+ onLongPressStart: onLongPressStart,
+ );
+ }
+
+ @override
+ void updateRenderObject(BuildContext context, RenderImageGrid renderObject) {
+ renderObject
+ ..onTap = onTap
+ ..onSecondaryTapUp = onSecondaryTapUp
+ ..onLongPressStart = onLongPressStart;
+ }
+}
+
+typedef OnShowMenu = Function(int index, Offset offset);
+
+class RenderImageGrid extends RenderBox
+ with
+ ContainerRenderObjectMixin,
+ RenderBoxContainerDefaultsMixin,
+ RenderObjectWithLayoutCallbackMixin {
+ RenderImageGrid({
+ required ValueChanged onTap,
+ required OnShowMenu? onSecondaryTapUp,
+ required OnShowMenu? onLongPressStart,
+ }) : _onTap = onTap,
+ _onSecondaryTapUp = onSecondaryTapUp,
+ _onLongPressStart = onLongPressStart {
+ _tapGestureRecognizer = TapGestureRecognizer()..onTap = _handleOnTap;
+ if (onSecondaryTapUp != null) {
+ _tapGestureRecognizer.onSecondaryTapUp = _handleSecondaryTapUp;
+ }
+ if (onLongPressStart != null) {
+ _longPressGestureRecognizer = LongPressGestureRecognizer()
+ ..onLongPressStart = _handleLongPressStart;
+ }
+ }
+
+ ValueChanged _onTap;
+ set onTap(ValueChanged value) {
+ _onTap = value;
+ }
+
+ OnShowMenu? _onSecondaryTapUp;
+ set onSecondaryTapUp(OnShowMenu? value) {
+ _onSecondaryTapUp = value;
+ }
+
+ OnShowMenu? _onLongPressStart;
+ set onLongPressStart(OnShowMenu? value) {
+ _onLongPressStart = value;
+ }
+
+ int? _index;
+
+ void _handleOnTap() {
+ _onTap(_index!);
+ }
+
+ void _handleSecondaryTapUp(TapUpDetails details) {
+ _onSecondaryTapUp!(_index!, details.globalPosition);
+ }
+
+ void _handleLongPressStart(LongPressStartDetails details) {
+ _onLongPressStart!(_index!, details.globalPosition);
+ }
+
+ @override
+ void setupParentData(RenderBox child) {
+ if (child.parentData is! MultiChildLayoutParentData) {
+ child.parentData = MultiChildLayoutParentData();
+ }
+ }
+
+ ImageGridInfo? imageGridInfo;
+ LayoutCallback? _callback;
+
+ void _updateCallback(LayoutCallback value) {
+ if (value == _callback) {
+ return;
+ }
+ _callback = value;
+ scheduleLayoutCallback();
+ }
+
+ @override
+ void layoutCallback() => _callback!(constraints);
+
+ @protected
+ BoxConstraints get layoutInfo => constraints;
+
+ @override
+ void performLayout() {
+ final BoxConstraints constraints = this.constraints;
+ runLayoutCallback();
+ final info = imageGridInfo!;
+ final row = info.row;
+ final column = info.column;
+ final width = info.size.width;
+ final height = info.size.height;
+ final childConstraints = BoxConstraints.tightFor(
+ width: width,
+ height: height,
+ );
+ RenderBox? child = firstChild;
+ while (child != null) {
+ child.layout(childConstraints);
+ final childParentData = child.parentData as MultiChildLayoutParentData;
+ final index = childParentData.id as int;
+ childParentData.offset = Offset(
+ (space + width) * (index % column),
+ (space + height) * (index ~/ column),
+ );
+ child = childParentData.nextSibling;
+ }
+ size = constraints.constrainDimensions(
+ width * column + space * (column - 1),
+ height * row + space * (row - 1),
+ );
+ }
+
+ @override
+ void paint(PaintingContext context, Offset offset) {
+ defaultPaint(context, offset);
+ }
+
+ @override
+ bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
+ RenderBox? child = lastChild;
+ while (child != null) {
+ final childParentData = child.parentData as MultiChildLayoutParentData;
+ final bool isHit = result.addWithPaintOffset(
+ offset: childParentData.offset,
+ position: position,
+ hitTest: (BoxHitTestResult result, Offset transformed) {
+ assert(transformed == position - childParentData.offset);
+ if (child!.size.contains(transformed)) {
+ result.add(BoxHitTestEntry(child, transformed));
+ return true;
+ }
+ return false;
+ },
+ );
+ if (isHit) {
+ _index = childParentData.id as int;
+ return true;
+ }
+ child = childParentData.previousSibling;
+ }
+ _index = null;
+ return false;
+ }
+
+ @override
+ void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
+ if (event is PointerDownEvent) {
+ _tapGestureRecognizer.addPointer(event);
+ _longPressGestureRecognizer?.addPointer(event);
+ }
+ }
+
+ late final TapGestureRecognizer _tapGestureRecognizer;
+ LongPressGestureRecognizer? _longPressGestureRecognizer;
+
+ @override
+ void dispose() {
+ _tapGestureRecognizer
+ ..onTap = null
+ ..onSecondaryTapUp = null
+ ..dispose();
+ _longPressGestureRecognizer
+ ?..onLongPressStart = null
+ ..dispose();
+ _longPressGestureRecognizer = null;
+ _onSecondaryTapUp = null;
+ _onLongPressStart = null;
+ super.dispose();
+ }
+
+ @override
+ bool get isRepaintBoundary => true; // gif repaint
+}
+
+class ImageGridRenderObjectElement extends RenderObjectElement {
+ ImageGridRenderObjectElement(ImageGridBuilder super.widget);
+
+ @override
+ RenderImageGrid get renderObject {
+ return super.renderObject as RenderImageGrid;
+ }
+
+ @protected
+ @visibleForTesting
+ Iterable get children =>
+ _children!.where((Element child) => !_forgottenChildren.contains(child));
+
+ List? _children;
+ // We keep a set of forgotten children to avoid O(n^2) work walking _children
+ // repeatedly to remove children.
+ final Set _forgottenChildren = HashSet();
+
+ @override
+ BuildScope get buildScope => _buildScope;
+
+ late final BuildScope _buildScope = BuildScope(
+ scheduleRebuild: _scheduleRebuild,
+ );
+
+ bool _deferredCallbackScheduled = false;
+ void _scheduleRebuild() {
+ if (_deferredCallbackScheduled) {
+ return;
+ }
+
+ final bool deferMarkNeedsLayout =
+ switch (SchedulerBinding.instance.schedulerPhase) {
+ SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true,
+ SchedulerPhase.transientCallbacks ||
+ SchedulerPhase.midFrameMicrotasks ||
+ SchedulerPhase.persistentCallbacks => false,
+ };
+ if (!deferMarkNeedsLayout) {
+ renderObject.scheduleLayoutCallback();
+ return;
+ }
+ _deferredCallbackScheduled = true;
+ SchedulerBinding.instance.scheduleFrameCallback(_frameCallback);
+ }
+
+ void _frameCallback(Duration timestamp) {
+ _deferredCallbackScheduled = false;
+ // This method is only called when the render tree is stable, if the Element
+ // is deactivated it will never be reincorporated back to the tree.
+ if (mounted) {
+ renderObject.scheduleLayoutCallback();
+ }
+ }
+
+ @override
+ void insertRenderObjectChild(RenderObject child, IndexedSlot slot) {
+ final ContainerRenderObjectMixin<
+ RenderObject,
+ ContainerParentDataMixin
+ >
+ renderObject = this.renderObject;
+ assert(renderObject.debugValidateChild(child));
+ renderObject.insert(child, after: slot.value?.renderObject);
+ assert(renderObject == this.renderObject);
+ }
+
+ @override
+ void moveRenderObjectChild(
+ RenderObject child,
+ IndexedSlot oldSlot,
+ IndexedSlot newSlot,
+ ) {
+ final ContainerRenderObjectMixin<
+ RenderObject,
+ ContainerParentDataMixin
+ >
+ renderObject = this.renderObject;
+ assert(child.parent == renderObject);
+ renderObject.move(child, after: newSlot.value?.renderObject);
+ assert(renderObject == this.renderObject);
+ }
+
+ @override
+ void removeRenderObjectChild(RenderObject child, Object? slot) {
+ final ContainerRenderObjectMixin<
+ RenderObject,
+ ContainerParentDataMixin
+ >
+ renderObject = this.renderObject;
+ assert(child.parent == renderObject);
+ renderObject.remove(child);
+ assert(renderObject == this.renderObject);
+ }
+
+ @override
+ void visitChildren(ElementVisitor visitor) {
+ if (_children == null) return;
+ for (final Element child in _children!) {
+ if (!_forgottenChildren.contains(child)) {
+ visitor(child);
+ }
+ }
+ }
+
+ @override
+ void forgetChild(Element child) {
+ if (_children == null) return;
+ assert(_children!.contains(child));
+ assert(!_forgottenChildren.contains(child));
+ _forgottenChildren.add(child);
+ super.forgetChild(child);
+ }
+
+ bool _debugCheckHasAssociatedRenderObject(Element newChild) {
+ assert(() {
+ if (newChild.renderObject == null) {
+ FlutterError.reportError(
+ FlutterErrorDetails(
+ exception: FlutterError.fromParts([
+ ErrorSummary(
+ 'The children of `MultiChildRenderObjectElement` must each has an associated render object.',
+ ),
+ ErrorHint(
+ 'This typically means that the `${newChild.widget}` or its children\n'
+ 'are not a subtype of `RenderObjectWidget`.',
+ ),
+ newChild.describeElement(
+ 'The following element does not have an associated render object',
+ ),
+ DiagnosticsDebugCreator(DebugCreator(newChild)),
+ ]),
+ ),
+ );
+ }
+ return true;
+ }());
+ return true;
+ }
+
+ @override
+ Element inflateWidget(Widget newWidget, Object? newSlot) {
+ final Element newChild = super.inflateWidget(newWidget, newSlot);
+ assert(_debugCheckHasAssociatedRenderObject(newChild));
+ return newChild;
+ }
+
+ @override
+ void mount(Element? parent, Object? newSlot) {
+ super.mount(parent, newSlot);
+ renderObject._updateCallback(_rebuildWithConstraints);
+ // final multiChildRenderObjectWidget = widget as MultiChildRenderObjectWidget;
+ // final children = List.filled(
+ // multiChildRenderObjectWidget.children.length,
+ // _NullElement.instance,
+ // );
+ // Element? previousChild;
+ // for (var i = 0; i < children.length; i += 1) {
+ // final Element newChild = inflateWidget(
+ // multiChildRenderObjectWidget.children[i],
+ // IndexedSlot(i, previousChild),
+ // );
+ // children[i] = newChild;
+ // previousChild = newChild;
+ // }
+ // _children = children;
+ }
+
+ @override
+ void update(ImageGridBuilder newWidget) {
+ super.update(newWidget);
+ final multiChildRenderObjectWidget = widget as ImageGridBuilder;
+ assert(widget == newWidget);
+ // _children = updateChildren(
+ // _children,
+ // multiChildRenderObjectWidget.children,
+ // forgottenChildren: _forgottenChildren,
+ // );
+ // _forgottenChildren.clear();
+ renderObject._updateCallback(_rebuildWithConstraints);
+ if (newWidget.updateShouldRebuild(multiChildRenderObjectWidget)) {
+ _needsBuild = true;
+ renderObject.scheduleLayoutCallback();
+ }
+ }
+
+ @override
+ void markNeedsBuild() {
+ // Calling super.markNeedsBuild is not needed. This Element does not need
+ // to performRebuild since this call already does what performRebuild does,
+ // So the element is clean as soon as this method returns and does not have
+ // to be added to the dirty list or marked as dirty.
+ renderObject.scheduleLayoutCallback();
+ _needsBuild = true;
+ }
+
+ @override
+ void performRebuild() {
+ // This gets called if markNeedsBuild() is called on us.
+ // That might happen if, e.g., our builder uses Inherited widgets.
+
+ // Force the callback to be called, even if the layout constraints are the
+ // same. This is because that callback may depend on the updated widget
+ // configuration, or an inherited widget.
+ renderObject.scheduleLayoutCallback();
+ _needsBuild = true;
+ super
+ .performRebuild(); // Calls widget.updateRenderObject (a no-op in this case).
+ }
+
+ @override
+ void unmount() {
+ renderObject._callback = null;
+ super.unmount();
+ }
+
+ // The LayoutInfoType that was used to invoke the layout callback with last time,
+ // during layout. The `_previousLayoutInfo` value is compared to the new one
+ // to determine whether [LayoutBuilderBase.builder] needs to be called.
+ BoxConstraints? _previousLayoutInfo;
+ bool _needsBuild = true;
+
+ static ImageGridInfo _calcGridInfo(
+ List picArr,
+ BoxConstraints layoutInfo,
+ ) {
+ final maxWidth = layoutInfo.maxWidth;
+ double imageWidth;
+ double imageHeight;
+ final length = picArr.length;
+ final isSingle = length == 1;
+ final isFour = length == 4;
+ if (length == 2) {
+ imageWidth = imageHeight = (maxWidth - space) / 2;
+ } else {
+ imageHeight = imageWidth = (maxWidth - 2 * space) / 3;
+ if (isSingle) {
+ final img = picArr.first;
+ final width = img.width;
+ final height = img.height;
+ final ratioWH = width / height;
+ final ratioHW = height / width;
+ imageWidth = ratioWH > 1.5
+ ? maxWidth
+ : (ratioWH >= 1 || (height > width && ratioHW < 1.5))
+ ? 2 * imageWidth
+ : 1.5 * imageWidth;
+ if (width != 1) {
+ imageWidth = math.min(imageWidth, width.toDouble());
+ }
+ imageHeight = imageWidth * math.min(ratioHW, StyleString.imgMaxRatio);
+ }
+ }
+
+ final int column = isFour ? 2 : 3;
+ final int row = isFour ? 2 : (length / 3).ceil();
+
+ return (
+ row: row,
+ column: column,
+ size: Size(imageWidth, imageHeight),
+ );
+ }
+
+ void _rebuildWithConstraints(Constraints _) {
+ final BoxConstraints layoutInfo = renderObject.layoutInfo;
+ @pragma('vm:notify-debugger-on-exception')
+ void updateChildCallback() {
+ List built;
+ try {
+ assert(layoutInfo == renderObject.layoutInfo);
+ built = (widget as ImageGridBuilder).builder(
+ this,
+ renderObject.imageGridInfo = _calcGridInfo(
+ (widget as ImageGridBuilder).picArr,
+ layoutInfo,
+ ),
+ );
+ } catch (e, stack) {
+ built = [
+ ErrorWidget.builder(
+ _reportException(
+ ErrorDescription('building $widget'),
+ e,
+ stack,
+ informationCollector: () => [
+ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)),
+ ],
+ ),
+ ),
+ ];
+ }
+ try {
+ if (_children == null) {
+ final children = List.filled(
+ built.length,
+ _NullElement.instance,
+ );
+ Element? previousChild;
+ for (var i = 0; i < children.length; i += 1) {
+ final Element newChild = inflateWidget(
+ built[i],
+ IndexedSlot(i, previousChild),
+ );
+ children[i] = newChild;
+ previousChild = newChild;
+ }
+ _children = children;
+ } else {
+ _children = updateChildren(
+ _children!,
+ built,
+ forgottenChildren: _forgottenChildren,
+ );
+ }
+ } catch (e, stack) {
+ built = [
+ ErrorWidget.builder(
+ _reportException(
+ ErrorDescription('building $widget'),
+ e,
+ stack,
+ informationCollector: () => [
+ if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)),
+ ],
+ ),
+ ),
+ ];
+ _children = updateChildren([], built);
+ } finally {
+ _needsBuild = false;
+ _previousLayoutInfo = layoutInfo;
+ _forgottenChildren.clear();
+ }
+ }
+
+ final VoidCallback? callback =
+ _needsBuild || (layoutInfo != _previousLayoutInfo)
+ ? updateChildCallback
+ : null;
+ owner!.buildScope(this, callback);
+ }
+}
+
+FlutterErrorDetails _reportException(
+ DiagnosticsNode context,
+ Object exception,
+ StackTrace stack, {
+ InformationCollector? informationCollector,
+}) {
+ final details = FlutterErrorDetails(
+ exception: exception,
+ stack: stack,
+ library: 'widgets library',
+ context: context,
+ informationCollector: informationCollector,
+ );
+ FlutterError.reportError(details);
+ return details;
+}
+
+class _NullElement extends Element {
+ _NullElement() : super(const _NullWidget());
+
+ static _NullElement instance = _NullElement();
+
+ @override
+ bool get debugDoingBuild => throw UnimplementedError();
+}
+
+class _NullWidget extends Widget {
+ const _NullWidget();
+
+ @override
+ Element createElement() => throw UnimplementedError();
+}
diff --git a/lib/common/widgets/image_grid/image_grid_view.dart b/lib/common/widgets/image_grid/image_grid_view.dart
new file mode 100644
index 000000000..a29fb6297
--- /dev/null
+++ b/lib/common/widgets/image_grid/image_grid_view.dart
@@ -0,0 +1,271 @@
+/*
+ * This file is part of PiliPlus
+ *
+ * PiliPlus is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * PiliPlus is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with PiliPlus. If not, see .
+ */
+
+import 'dart:io' show Platform;
+
+import 'package:PiliPlus/common/constants.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/models/common/badge_type.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/image_utils.dart';
+import 'package:PiliPlus/utils/page_utils.dart';
+import 'package:PiliPlus/utils/platform_utils.dart';
+import 'package:PiliPlus/utils/storage_pref.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart' show HapticFeedback;
+import 'package:get/get_core/src/get_main.dart';
+import 'package:get/get_navigation/get_navigation.dart';
+
+class ImageModel {
+ ImageModel({
+ required num? width,
+ required num? height,
+ required this.url,
+ this.liveUrl,
+ }) {
+ this.width = width == null || width == 0 ? 1 : width;
+ this.height = height == null || height == 0 ? 1 : height;
+ }
+
+ late num width;
+ late num height;
+ String url;
+ String? liveUrl;
+ bool? _isLongPic;
+ bool? _isLivePhoto;
+
+ bool get isLongPic =>
+ _isLongPic ??= (height / width) > StyleString.imgMaxRatio;
+ bool get isLivePhoto =>
+ _isLivePhoto ??= enableLivePhoto && liveUrl?.isNotEmpty == true;
+
+ static bool enableLivePhoto = Pref.enableLivePhoto;
+}
+
+class ImageGridView extends StatelessWidget {
+ const ImageGridView({
+ super.key,
+ required this.picArr,
+ this.onViewImage,
+ this.fullScreen = false,
+ });
+
+ final List picArr;
+ final VoidCallback? onViewImage;
+ final bool fullScreen;
+
+ static bool horizontalPreview = Pref.horizontalPreview;
+ static const _routes = ['/videoV', '/dynamicDetail'];
+
+ void _onTap(BuildContext context, int index) {
+ final imgList = picArr.map(
+ (item) {
+ bool isLive = item.isLivePhoto;
+ return SourceModel(
+ sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage,
+ url: item.url,
+ liveUrl: isLive ? item.liveUrl : null,
+ width: isLive ? item.width.toInt() : null,
+ height: isLive ? item.height.toInt() : null,
+ isLongPic: item.isLongPic,
+ );
+ },
+ ).toList();
+ if (horizontalPreview &&
+ !fullScreen &&
+ _routes.contains(Get.currentRoute) &&
+ !context.mediaQuerySize.isPortrait) {
+ final scaffoldState = Scaffold.maybeOf(context);
+ if (scaffoldState != null) {
+ onViewImage?.call();
+ PageUtils.onHorizontalPreviewState(
+ scaffoldState,
+ imgList,
+ index,
+ );
+ return;
+ }
+ }
+ PageUtils.imageView(
+ initialPage: index,
+ imgList: imgList,
+ );
+ }
+
+ static BorderRadius _borderRadius(
+ int col,
+ int length,
+ int index, {
+ Radius r = StyleString.imgRadius,
+ }) {
+ if (length == 1) return StyleString.mdRadius;
+
+ final bool hasUp = index - col >= 0;
+ final bool hasDown = index + col < length;
+
+ final bool isRowStart = (index % col) == 0;
+ final bool isRowEnd = (index % col) == col - 1 || index == length - 1;
+
+ final bool hasLeft = !isRowStart;
+ final bool hasRight = !isRowEnd && (index + 1) < length;
+
+ return BorderRadius.only(
+ topLeft: !hasUp && !hasLeft ? r : Radius.zero,
+ topRight: !hasUp && !hasRight ? r : Radius.zero,
+ bottomLeft: !hasDown && !hasLeft ? r : Radius.zero,
+ bottomRight: !hasDown && !hasRight ? r : Radius.zero,
+ );
+ }
+
+ static bool enableImgMenu = Pref.enableImgMenu;
+
+ void _showMenu(BuildContext context, int index, Offset offset) {
+ HapticFeedback.mediumImpact();
+ final item = picArr[index];
+ showMenu(
+ context: context,
+ position: PageUtils.menuPosition(offset),
+ items: [
+ if (PlatformUtils.isMobile)
+ PopupMenuItem(
+ height: 42,
+ onTap: () => ImageUtils.onShareImg(item.url),
+ child: const Text('分享', style: TextStyle(fontSize: 14)),
+ ),
+ PopupMenuItem(
+ height: 42,
+ onTap: () => ImageUtils.downloadImg([item.url]),
+ child: const Text('保存图片', style: TextStyle(fontSize: 14)),
+ ),
+ if (PlatformUtils.isDesktop)
+ PopupMenuItem(
+ height: 42,
+ onTap: () => PageUtils.launchURL(item.url),
+ child: const Text('网页打开', style: TextStyle(fontSize: 14)),
+ )
+ else if (picArr.length > 1)
+ PopupMenuItem(
+ height: 42,
+ onTap: () =>
+ ImageUtils.downloadImg(picArr.map((item) => item.url).toList()),
+ child: const Text('保存全部', style: TextStyle(fontSize: 14)),
+ ),
+ if (item.isLivePhoto)
+ PopupMenuItem(
+ height: 42,
+ onTap: () => ImageUtils.downloadLivePhoto(
+ url: item.url,
+ liveUrl: item.liveUrl!,
+ width: item.width.toInt(),
+ height: item.height.toInt(),
+ ),
+ child: Text(
+ '保存${Platform.isIOS ? '实况' : '视频'}',
+ style: const TextStyle(fontSize: 14),
+ ),
+ ),
+ ],
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const .only(top: 6),
+ child: ImageGridBuilder(
+ picArr: picArr,
+ onTap: (index) => _onTap(context, index),
+ onSecondaryTapUp: enableImgMenu && PlatformUtils.isDesktop
+ ? (index, offset) => _showMenu(context, index, offset)
+ : null,
+ onLongPressStart: enableImgMenu && PlatformUtils.isMobile
+ ? (index, offset) => _showMenu(context, index, offset)
+ : null,
+ builder: (BuildContext context, ImageGridInfo info) {
+ final width = info.size.width;
+ final height = info.size.height;
+ late final placeHolder = Container(
+ width: width,
+ height: height,
+ decoration: BoxDecoration(
+ color: Theme.of(
+ context,
+ ).colorScheme.onInverseSurface.withValues(alpha: 0.4),
+ ),
+ child: Image.asset(
+ 'assets/images/loading.png',
+ width: width,
+ height: height,
+ cacheWidth: width.cacheSize(context),
+ ),
+ );
+ return List.generate(picArr.length, (index) {
+ final item = picArr[index];
+ final borderRadius = _borderRadius(
+ info.column,
+ picArr.length,
+ index,
+ );
+ Widget child = Stack(
+ clipBehavior: Clip.none,
+ alignment: Alignment.center,
+ children: [
+ NetworkImgLayer(
+ src: item.url,
+ width: width,
+ height: height,
+ borderRadius: borderRadius,
+ alignment: item.isLongPic ? .topCenter : .center,
+ cacheWidth: item.width <= item.height,
+ getPlaceHolder: () => placeHolder,
+ ),
+ if (item.isLivePhoto)
+ const PBadge(
+ text: 'Live',
+ right: 8,
+ bottom: 8,
+ type: PBadgeType.gray,
+ )
+ else if (item.isLongPic)
+ const PBadge(
+ text: '长图',
+ right: 8,
+ bottom: 8,
+ ),
+ ],
+ );
+ if (!item.isLongPic) {
+ child = Hero(
+ tag: item.url,
+ child: child,
+ );
+ }
+ return LayoutId(
+ id: index,
+ child: child,
+ );
+ });
+ },
+ ),
+ );
+ }
+}
diff --git a/lib/pages/article/widgets/opus_content.dart b/lib/pages/article/widgets/opus_content.dart
index 825f04ee6..ae3d13be6 100644
--- a/lib/pages/article/widgets/opus_content.dart
+++ b/lib/pages/article/widgets/opus_content.dart
@@ -2,8 +2,8 @@ import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image/cached_network_svg_image.dart';
-import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
+import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
import 'package:PiliPlus/common/widgets/image_viewer/hero.dart';
import 'package:PiliPlus/http/constants.dart';
import 'package:PiliPlus/models/common/image_preview_type.dart';
@@ -247,8 +247,7 @@ class OpusContent extends StatelessWidget {
child: child,
);
} else {
- return CustomGridView(
- maxWidth: maxWidth,
+ return ImageGridView(
picArr: element.pic!.pics!
.map(
(e) => ImageModel(
diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart
index 2991fc6d2..46d056db9 100644
--- a/lib/pages/dynamics/widgets/author_panel.dart
+++ b/lib/pages/dynamics/widgets/author_panel.dart
@@ -293,9 +293,7 @@ class AuthorPanel extends StatelessWidget {
height: 3,
decoration: BoxDecoration(
color: theme.colorScheme.outline,
- borderRadius: const BorderRadius.all(
- Radius.circular(3),
- ),
+ borderRadius: const .all(.circular(1.5)),
),
),
),
diff --git a/lib/pages/dynamics/widgets/content_panel.dart b/lib/pages/dynamics/widgets/content_panel.dart
index 3f8dd400d..41b2a2c18 100644
--- a/lib/pages/dynamics/widgets/content_panel.dart
+++ b/lib/pages/dynamics/widgets/content_panel.dart
@@ -1,7 +1,7 @@
// 内容
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/text/text.dart' as custom_text;
-import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
+import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
import 'package:PiliPlus/utils/page_utils.dart';
@@ -94,8 +94,7 @@ Widget content(
primary: theme.colorScheme.primary,
),
if (pics != null && pics.isNotEmpty)
- CustomGridView(
- maxWidth: maxWidth,
+ ImageGridView(
picArr: pics
.map(
(item) => ImageModel(
diff --git a/lib/pages/dynamics/widgets/rich_node_panel.dart b/lib/pages/dynamics/widgets/rich_node_panel.dart
index b298aa304..0c9b67903 100644
--- a/lib/pages/dynamics/widgets/rich_node_panel.dart
+++ b/lib/pages/dynamics/widgets/rich_node_panel.dart
@@ -1,8 +1,8 @@
import 'dart:io' show Platform;
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
-import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
+import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/search.dart';
@@ -252,9 +252,8 @@ TextSpan? richNode(
..add(const TextSpan(text: '\n'))
..add(
WidgetSpan(
- child: CustomGridView(
+ child: ImageGridView(
fullScreen: true,
- maxWidth: maxWidth,
picArr: i.pics!
.map(
(item) => ImageModel(
diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart
index 396541da0..a7851acda 100644
--- a/lib/pages/setting/models/extra_settings.dart
+++ b/lib/pages/setting/models/extra_settings.dart
@@ -5,8 +5,8 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'
show touchSlopH;
-import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
- show CustomGridView, ImageModel;
+import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart'
+ show ImageGridView, ImageModel;
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/grpc/reply.dart';
import 'package:PiliPlus/http/fav.dart';
@@ -158,7 +158,7 @@ List get extraSettings => [
leading: const Icon(Icons.photo_outlined),
setKey: SettingBoxKey.horizontalPreview,
defaultVal: false,
- onChanged: (value) => CustomGridView.horizontalPreview = value,
+ onChanged: (value) => ImageGridView.horizontalPreview = value,
),
NormalModel(
title: '评论折叠行数',
@@ -468,7 +468,7 @@ List get extraSettings => [
leading: const Icon(Icons.menu),
setKey: SettingBoxKey.enableImgMenu,
defaultVal: false,
- onChanged: (value) => CustomGridView.enableImgMenu = value,
+ onChanged: (value) => ImageGridView.enableImgMenu = value,
),
SwitchModel(
setKey: SettingBoxKey.feedBackEnable,
diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart
index cdb96b463..65cba75d6 100644
--- a/lib/pages/video/reply/widgets/reply_item_grpc.dart
+++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart
@@ -5,8 +5,8 @@ import 'package:PiliPlus/common/widgets/badge.dart';
import 'package:PiliPlus/common/widgets/dialog/report.dart';
import 'package:PiliPlus/common/widgets/flutter/text/text.dart' as custom_text;
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
-import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
+import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart';
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
show ReplyInfo, ReplyControl, Content, Url;
@@ -298,20 +298,17 @@ class ReplyItemGrpc extends StatelessWidget {
if (replyItem.content.pictures.isNotEmpty) ...[
Padding(
padding: padding,
- child: LayoutBuilder(
- builder: (context, constraints) => CustomGridView(
- maxWidth: constraints.maxWidth,
- picArr: replyItem.content.pictures
- .map(
- (item) => ImageModel(
- width: item.imgWidth,
- height: item.imgHeight,
- url: item.imgSrc,
- ),
- )
- .toList(),
- onViewImage: onViewImage,
- ),
+ child: ImageGridView(
+ picArr: replyItem.content.pictures
+ .map(
+ (item) => ImageModel(
+ width: item.imgWidth,
+ height: item.imgHeight,
+ url: item.imgSrc,
+ ),
+ )
+ .toList(),
+ onViewImage: onViewImage,
),
),
const SizedBox(height: 4),