mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-07-18 09:36:52 +08:00
@@ -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 <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
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<ImageModel> 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<int> 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<RenderBox, MultiChildLayoutParentData>,
|
|
||||||
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
|
|
||||||
RenderImageGrid({
|
|
||||||
required double space,
|
|
||||||
required int column,
|
|
||||||
required double width,
|
|
||||||
required double height,
|
|
||||||
required ValueChanged<int> 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<int> _onTap;
|
|
||||||
set onTap(ValueChanged<int> 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
|
|
||||||
}
|
|
||||||
634
lib/common/widgets/image_grid/image_grid_builder.dart
Normal file
634
lib/common/widgets/image_grid/image_grid_builder.dart
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<ImageModel> picArr;
|
||||||
|
final ValueChanged<int> onTap;
|
||||||
|
final OnShowMenu? onSecondaryTapUp;
|
||||||
|
final OnShowMenu? onLongPressStart;
|
||||||
|
final List<Widget> 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<RenderBox, MultiChildLayoutParentData>,
|
||||||
|
RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData>,
|
||||||
|
RenderObjectWithLayoutCallbackMixin {
|
||||||
|
RenderImageGrid({
|
||||||
|
required ValueChanged<int> 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<int> _onTap;
|
||||||
|
set onTap(ValueChanged<int> 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<Constraints>? _callback;
|
||||||
|
|
||||||
|
void _updateCallback(LayoutCallback<Constraints> 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<Element> get children =>
|
||||||
|
_children!.where((Element child) => !_forgottenChildren.contains(child));
|
||||||
|
|
||||||
|
List<Element>? _children;
|
||||||
|
// We keep a set of forgotten children to avoid O(n^2) work walking _children
|
||||||
|
// repeatedly to remove children.
|
||||||
|
final Set<Element> _forgottenChildren = HashSet<Element>();
|
||||||
|
|
||||||
|
@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<Element?> slot) {
|
||||||
|
final ContainerRenderObjectMixin<
|
||||||
|
RenderObject,
|
||||||
|
ContainerParentDataMixin<RenderObject>
|
||||||
|
>
|
||||||
|
renderObject = this.renderObject;
|
||||||
|
assert(renderObject.debugValidateChild(child));
|
||||||
|
renderObject.insert(child, after: slot.value?.renderObject);
|
||||||
|
assert(renderObject == this.renderObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void moveRenderObjectChild(
|
||||||
|
RenderObject child,
|
||||||
|
IndexedSlot<Element?> oldSlot,
|
||||||
|
IndexedSlot<Element?> newSlot,
|
||||||
|
) {
|
||||||
|
final ContainerRenderObjectMixin<
|
||||||
|
RenderObject,
|
||||||
|
ContainerParentDataMixin<RenderObject>
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
>
|
||||||
|
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(<DiagnosticsNode>[
|
||||||
|
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<Element>.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<Element?>(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<ImageModel> 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<Widget> 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: () => <DiagnosticsNode>[
|
||||||
|
if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (_children == null) {
|
||||||
|
final children = List<Element>.filled(
|
||||||
|
built.length,
|
||||||
|
_NullElement.instance,
|
||||||
|
);
|
||||||
|
Element? previousChild;
|
||||||
|
for (var i = 0; i < children.length; i += 1) {
|
||||||
|
final Element newChild = inflateWidget(
|
||||||
|
built[i],
|
||||||
|
IndexedSlot<Element?>(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: () => <DiagnosticsNode>[
|
||||||
|
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();
|
||||||
|
}
|
||||||
271
lib/common/widgets/image_grid/image_grid_view.dart
Normal file
271
lib/common/widgets/image_grid/image_grid_view.dart
Normal file
@@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<ImageModel> 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,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ import 'dart:math' as math;
|
|||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
|
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/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/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/common/widgets/image_viewer/hero.dart';
|
||||||
import 'package:PiliPlus/http/constants.dart';
|
import 'package:PiliPlus/http/constants.dart';
|
||||||
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
import 'package:PiliPlus/models/common/image_preview_type.dart';
|
||||||
@@ -247,8 +247,7 @@ class OpusContent extends StatelessWidget {
|
|||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return CustomGridView(
|
return ImageGridView(
|
||||||
maxWidth: maxWidth,
|
|
||||||
picArr: element.pic!.pics!
|
picArr: element.pic!.pics!
|
||||||
.map(
|
.map(
|
||||||
(e) => ImageModel(
|
(e) => ImageModel(
|
||||||
|
|||||||
@@ -293,9 +293,7 @@ class AuthorPanel extends StatelessWidget {
|
|||||||
height: 3,
|
height: 3,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.outline,
|
color: theme.colorScheme.outline,
|
||||||
borderRadius: const BorderRadius.all(
|
borderRadius: const .all(.circular(1.5)),
|
||||||
Radius.circular(3),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 内容
|
// 内容
|
||||||
import 'package:PiliPlus/common/widgets/custom_icon.dart';
|
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/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/models/dynamics/result.dart';
|
||||||
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
|
import 'package:PiliPlus/pages/dynamics/widgets/rich_node_panel.dart';
|
||||||
import 'package:PiliPlus/utils/page_utils.dart';
|
import 'package:PiliPlus/utils/page_utils.dart';
|
||||||
@@ -94,8 +94,7 @@ Widget content(
|
|||||||
primary: theme.colorScheme.primary,
|
primary: theme.colorScheme.primary,
|
||||||
),
|
),
|
||||||
if (pics != null && pics.isNotEmpty)
|
if (pics != null && pics.isNotEmpty)
|
||||||
CustomGridView(
|
ImageGridView(
|
||||||
maxWidth: maxWidth,
|
|
||||||
picArr: pics
|
picArr: pics
|
||||||
.map(
|
.map(
|
||||||
(item) => ImageModel(
|
(item) => ImageModel(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
|
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/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/dynamics.dart';
|
||||||
import 'package:PiliPlus/http/loading_state.dart';
|
import 'package:PiliPlus/http/loading_state.dart';
|
||||||
import 'package:PiliPlus/http/search.dart';
|
import 'package:PiliPlus/http/search.dart';
|
||||||
@@ -252,9 +252,8 @@ TextSpan? richNode(
|
|||||||
..add(const TextSpan(text: '\n'))
|
..add(const TextSpan(text: '\n'))
|
||||||
..add(
|
..add(
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: CustomGridView(
|
child: ImageGridView(
|
||||||
fullScreen: true,
|
fullScreen: true,
|
||||||
maxWidth: maxWidth,
|
|
||||||
picArr: i.pics!
|
picArr: i.pics!
|
||||||
.map(
|
.map(
|
||||||
(item) => ImageModel(
|
(item) => ImageModel(
|
||||||
|
|||||||
@@ -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/flutter/refresh_indicator.dart';
|
||||||
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'
|
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'
|
||||||
show touchSlopH;
|
show touchSlopH;
|
||||||
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
|
import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart'
|
||||||
show CustomGridView, ImageModel;
|
show ImageGridView, ImageModel;
|
||||||
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
|
||||||
import 'package:PiliPlus/grpc/reply.dart';
|
import 'package:PiliPlus/grpc/reply.dart';
|
||||||
import 'package:PiliPlus/http/fav.dart';
|
import 'package:PiliPlus/http/fav.dart';
|
||||||
@@ -158,7 +158,7 @@ List<SettingsModel> get extraSettings => [
|
|||||||
leading: const Icon(Icons.photo_outlined),
|
leading: const Icon(Icons.photo_outlined),
|
||||||
setKey: SettingBoxKey.horizontalPreview,
|
setKey: SettingBoxKey.horizontalPreview,
|
||||||
defaultVal: false,
|
defaultVal: false,
|
||||||
onChanged: (value) => CustomGridView.horizontalPreview = value,
|
onChanged: (value) => ImageGridView.horizontalPreview = value,
|
||||||
),
|
),
|
||||||
NormalModel(
|
NormalModel(
|
||||||
title: '评论折叠行数',
|
title: '评论折叠行数',
|
||||||
@@ -468,7 +468,7 @@ List<SettingsModel> get extraSettings => [
|
|||||||
leading: const Icon(Icons.menu),
|
leading: const Icon(Icons.menu),
|
||||||
setKey: SettingBoxKey.enableImgMenu,
|
setKey: SettingBoxKey.enableImgMenu,
|
||||||
defaultVal: false,
|
defaultVal: false,
|
||||||
onChanged: (value) => CustomGridView.enableImgMenu = value,
|
onChanged: (value) => ImageGridView.enableImgMenu = value,
|
||||||
),
|
),
|
||||||
SwitchModel(
|
SwitchModel(
|
||||||
setKey: SettingBoxKey.feedBackEnable,
|
setKey: SettingBoxKey.feedBackEnable,
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import 'package:PiliPlus/common/widgets/badge.dart';
|
|||||||
import 'package:PiliPlus/common/widgets/dialog/report.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/flutter/text/text.dart' as custom_text;
|
||||||
import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart';
|
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/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/common/widgets/pendant_avatar.dart';
|
||||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||||
show ReplyInfo, ReplyControl, Content, Url;
|
show ReplyInfo, ReplyControl, Content, Url;
|
||||||
@@ -298,20 +298,17 @@ class ReplyItemGrpc extends StatelessWidget {
|
|||||||
if (replyItem.content.pictures.isNotEmpty) ...[
|
if (replyItem.content.pictures.isNotEmpty) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: padding,
|
padding: padding,
|
||||||
child: LayoutBuilder(
|
child: ImageGridView(
|
||||||
builder: (context, constraints) => CustomGridView(
|
picArr: replyItem.content.pictures
|
||||||
maxWidth: constraints.maxWidth,
|
.map(
|
||||||
picArr: replyItem.content.pictures
|
(item) => ImageModel(
|
||||||
.map(
|
width: item.imgWidth,
|
||||||
(item) => ImageModel(
|
height: item.imgHeight,
|
||||||
width: item.imgWidth,
|
url: item.imgSrc,
|
||||||
height: item.imgHeight,
|
),
|
||||||
url: item.imgSrc,
|
)
|
||||||
),
|
.toList(),
|
||||||
)
|
onViewImage: onViewImage,
|
||||||
.toList(),
|
|
||||||
onViewImage: onViewImage,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
Reference in New Issue
Block a user