improve image viewer

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-05-16 18:02:51 +08:00
parent 6981cb65d7
commit f84078d681
2 changed files with 139 additions and 60 deletions

View File

@@ -16,6 +16,7 @@
*/ */
import 'dart:io' show File, Platform; import 'dart:io' show File, Platform;
import 'dart:math' as math;
import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; import 'package:PiliPlus/common/widgets/colored_box_transition.dart';
import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart';
@@ -33,6 +34,7 @@ import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:PiliPlus/utils/theme_utils.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_debounce/easy_throttle.dart'; import 'package:easy_debounce/easy_throttle.dart';
@@ -52,7 +54,7 @@ class GalleryViewer extends StatefulWidget {
super.key, super.key,
this.minScale = 1.0, this.minScale = 1.0,
this.maxScale = 8.0, this.maxScale = 8.0,
required this.quality, required this.thumbQ,
required this.sources, required this.sources,
this.initIndex = 0, this.initIndex = 0,
this.onPageChanged, this.onPageChanged,
@@ -61,7 +63,7 @@ class GalleryViewer extends StatefulWidget {
final double minScale; final double minScale;
final double maxScale; final double maxScale;
final int quality; final int thumbQ;
final List<SourceModel> sources; final List<SourceModel> sources;
final int initIndex; final int initIndex;
final ValueChanged<int>? onPageChanged; final ValueChanged<int>? onPageChanged;
@@ -73,8 +75,11 @@ class GalleryViewer extends StatefulWidget {
class _GalleryViewerState extends State<GalleryViewer> class _GalleryViewerState extends State<GalleryViewer>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
late final sources = widget.sources;
late Size _containerSize; late Size _containerSize;
late final int _quality; late final int _previewQ;
late final bool _isOrigin;
List<bool>? _isOrigPic;
late final RxInt _currIndex; late final RxInt _currIndex;
GlobalKey? _key; GlobalKey? _key;
late EdgeInsets _padding; late EdgeInsets _padding;
@@ -98,10 +103,10 @@ class _GalleryViewerState extends State<GalleryViewer>
Offset _offset = Offset.zero; Offset _offset = Offset.zero;
bool _dragging = false; bool _dragging = false;
String _getActualUrl(String url) { String _getActualUrl(String url, {required int index}) {
return _quality != 100 return _isOrigin || _isOrigPic![index]
? ImageUtils.thumbnailUrl(url, maxQuality: _quality) ? url.http2https
: url.http2https; : ImageUtils.thumbnailUrl(url, maxQuality: _previewQ);
} }
Future<void> _initPlayer() async { Future<void> _initPlayer() async {
@@ -114,7 +119,7 @@ class _GalleryViewerState extends State<GalleryViewer>
return; return;
} }
_player = player; _player = player;
final currItem = widget.sources[_currIndex.value]; final currItem = sources[_currIndex.value];
if (currItem.sourceType == .livePhoto) { if (currItem.sourceType == .livePhoto) {
player.open(Media(currItem.liveUrl!)); player.open(Media(currItem.liveUrl!));
_currIndex.refresh(); _currIndex.refresh();
@@ -124,9 +129,13 @@ class _GalleryViewerState extends State<GalleryViewer>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_quality = Pref.previewQ; _previewQ = Pref.previewQ;
_isOrigin = _previewQ == 100;
if (!_isOrigin) {
_isOrigPic = List.filled(sources.length, false);
}
_currIndex = widget.initIndex.obs; _currIndex = widget.initIndex.obs;
final item = widget.sources[widget.initIndex]; final item = sources[widget.initIndex];
_playIfNeeded(item); _playIfNeeded(item);
if (!item.isLongPic) { if (!item.isLongPic) {
@@ -258,13 +267,27 @@ class _GalleryViewerState extends State<GalleryViewer>
..onDoubleTap = null ..onDoubleTap = null
..dispose(); ..dispose();
_longPressGestureRecognizer.dispose(); _longPressGestureRecognizer.dispose();
if (widget.quality != _quality) { if (_isOrigPic != null) {
for (final item in widget.sources) { for (int i = 0; i < _isOrigPic!.length; i++) {
if (item.sourceType == SourceType.networkImage) { if (_isOrigPic![i]) {
CachedNetworkImageProvider(_getActualUrl(item.url)).evict(); final item = sources[i];
if (item.sourceType == .networkImage) {
CachedNetworkImageProvider(
_getActualUrl(item.url, index: i),
).evict();
} }
} }
} }
}
if (widget.thumbQ != _previewQ) {
for (int i = 0; i < sources.length; i++) {
final item = sources[i];
if (item.sourceType == .networkImage) {
CachedNetworkImageProvider(_getActualUrl(item.url, index: i)).evict();
}
}
}
Future.delayed(const Duration(milliseconds: 200), _currIndex.close); Future.delayed(const Duration(milliseconds: 200), _currIndex.close);
super.dispose(); super.dispose();
} }
@@ -299,7 +322,7 @@ class _GalleryViewerState extends State<GalleryViewer>
physics: const CustomTabBarViewScrollPhysics( physics: const CustomTabBarViewScrollPhysics(
parent: AlwaysScrollableScrollPhysics(), parent: AlwaysScrollableScrollPhysics(),
), ),
itemCount: widget.sources.length, itemCount: sources.length,
itemBuilder: _itemBuilder, itemBuilder: _itemBuilder,
horizontalDragGestureRecognizer: () => horizontalDragGestureRecognizer: () =>
_horizontalDragGestureRecognizer, _horizontalDragGestureRecognizer,
@@ -308,38 +331,86 @@ class _GalleryViewerState extends State<GalleryViewer>
}, },
), ),
_buildIndicator, _buildIndicator,
if (!_isOrigin) _originButton,
], ],
), ),
); );
} }
Widget get _buildIndicator => Positioned( Widget get _originButton {
return Positioned(
top: 18 + _padding.top,
child: Center(
child: Obx(() {
final index = _currIndex.value;
if (!_isOrigPic![index]) {
final item = sources[index];
if (item.sourceType == .networkImage) {
return FilledButton.tonal(
style: FilledButton.styleFrom(
minimumSize: .zero,
visualDensity: .standard,
tapTargetSize: .shrinkWrap,
padding: const .symmetric(horizontal: 8, vertical: 5.5),
shape: const RoundedRectangleBorder(
borderRadius: .all(.circular(4)),
),
backgroundColor:
ThemeUtils.darkTheme.colorScheme.secondaryContainer,
foregroundColor:
ThemeUtils.darkTheme.colorScheme.onSecondaryContainer,
),
onPressed: () {
_isOrigPic![index] = true;
setState(() {});
},
child: Text(
'查看原图${item.size == null ? '' : '(${item.size!.formatSize})'}',
style: const TextStyle(height: 1, fontSize: 13),
strutStyle: const StrutStyle(
height: 1,
leading: 0,
fontSize: 13,
),
),
);
}
}
return const SizedBox.shrink();
}),
),
);
}
Widget get _buildIndicator {
return Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
child: IgnorePointer( child: IgnorePointer(
child: Container( child: Container(
padding: _padding + const EdgeInsets.fromLTRB(12, 8, 20, 8), padding: _padding + const .fromLTRB(12, 8, 20, 8),
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: .topCenter,
end: Alignment.bottomCenter, end: .bottomCenter,
colors: [ colors: [
Colors.transparent, Colors.transparent,
Colors.black.withValues(alpha: 0.3), Colors.black.withValues(alpha: 0.3),
], ],
), ),
), ),
alignment: Alignment.center, alignment: .center,
child: Obx( child: Obx(
() => Text( () => Text(
"${_currIndex.value + 1}/${widget.sources.length}", "${_currIndex.value + 1}/${sources.length}",
style: const TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
), ),
), ),
), ),
); );
}
void _playIfNeeded(SourceModel item) { void _playIfNeeded(SourceModel item) {
if (item.sourceType == .livePhoto) { if (item.sourceType == .livePhoto) {
@@ -354,18 +425,18 @@ class _GalleryViewerState extends State<GalleryViewer>
void _onPageChanged(int index) { void _onPageChanged(int index) {
_player?.pause(); _player?.pause();
_playIfNeeded(widget.sources[index]); _playIfNeeded(sources[index]);
_currIndex.value = index; _currIndex.value = index;
widget.onPageChanged?.call(index); widget.onPageChanged?.call(index);
} }
late final ValueChanged<int>? _onChangePage = widget.sources.length == 1 late final ValueChanged<int>? _onChangePage = sources.length == 1
? null ? null
: (int offset) { : (int offset) {
final currPage = _pageController.page?.round() ?? 0; final currPage = _pageController.page?.round() ?? 0;
final nextPage = (currPage + offset).clamp( final nextPage = (currPage + offset).clamp(
0, 0,
widget.sources.length - 1, sources.length - 1,
); );
if (nextPage != currPage) { if (nextPage != currPage) {
_pageController.animateToPage( _pageController.animateToPage(
@@ -377,10 +448,10 @@ class _GalleryViewerState extends State<GalleryViewer>
}; };
Widget _itemBuilder(BuildContext context, int index) { Widget _itemBuilder(BuildContext context, int index) {
final item = widget.sources[index]; final item = sources[index];
final Widget child; final Widget child;
switch (item.sourceType) { switch (item.sourceType) {
case SourceType.fileImage: case .fileImage:
child = Image.file( child = Image.file(
key: _key, key: _key,
File(item.url), File(item.url),
@@ -395,14 +466,22 @@ class _GalleryViewerState extends State<GalleryViewer>
horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer, horizontalDragGestureRecognizer: _horizontalDragGestureRecognizer,
onChangePage: _onChangePage, onChangePage: _onChangePage,
); );
case SourceType.networkImage: case .networkImage:
final isLongPic = item.isLongPic; final isLongPic = item.isLongPic;
double? cacheWidth, cacheHeight;
if (item.isLongPic) {
cacheWidth = math.min(650.0, _containerSize.width);
} else if (_containerSize.width < _containerSize.height) {
cacheWidth = _containerSize.width;
} else {
cacheHeight = _containerSize.height;
}
child = Image( child = Image(
key: _key, key: _key,
image: ResizeImage.resizeIfNeeded( image: ResizeImage.resizeIfNeeded(
_containerSize.width, cacheWidth,
null, cacheHeight,
CachedNetworkImageProvider(_getActualUrl(item.url)), CachedNetworkImageProvider(_getActualUrl(item.url, index: index)),
), ),
minScale: widget.minScale, minScale: widget.minScale,
maxScale: widget.maxScale, maxScale: widget.maxScale,
@@ -415,17 +494,17 @@ class _GalleryViewerState extends State<GalleryViewer>
return child; return child;
} }
if (frame == null) { if (frame == null) {
if (widget.quality == _quality) { if (widget.thumbQ == _previewQ && _isOrigPic?[index] != true) {
return child; return child;
} else { } else {
return Image( return Image(
image: ResizeImage.resizeIfNeeded( image: ResizeImage.resizeIfNeeded(
_containerSize.width, cacheWidth,
null, cacheHeight,
CachedNetworkImageProvider( CachedNetworkImageProvider(
ImageUtils.thumbnailUrl( ImageUtils.thumbnailUrl(
item.url, item.url,
maxQuality: widget.quality, maxQuality: widget.thumbQ,
), ),
), ),
), ),
@@ -461,7 +540,7 @@ class _GalleryViewerState extends State<GalleryViewer>
if (isLongPic) { if (isLongPic) {
return child; return child;
} }
case SourceType.livePhoto: case .livePhoto:
child = Obx( child = Obx(
key: _key, key: _key,
() => _currIndex.value == index && _videoController != null () => _currIndex.value == index && _videoController != null
@@ -499,7 +578,7 @@ class _GalleryViewerState extends State<GalleryViewer>
} }
void _onLongPress() { void _onLongPress() {
final item = widget.sources[_currIndex.value]; final item = sources[_currIndex.value];
if (item.sourceType == .fileImage) return; if (item.sourceType == .fileImage) return;
HapticFeedback.mediumImpact(); HapticFeedback.mediumImpact();
showDialog( showDialog(
@@ -557,18 +636,18 @@ class _GalleryViewerState extends State<GalleryViewer>
dense: true, dense: true,
title: const Text('网页打开', style: TextStyle(fontSize: 14)), title: const Text('网页打开', style: TextStyle(fontSize: 14)),
) )
else if (widget.sources.length > 1) else if (sources.length > 1)
ListTile( ListTile(
onTap: () { onTap: () {
Get.back(); Get.back();
ImageUtils.downloadImg( ImageUtils.downloadImg(
widget.sources.map((item) => item.url).toList(), sources.map((item) => item.url).toList(),
); );
}, },
dense: true, dense: true,
title: const Text('保存全部图片', style: TextStyle(fontSize: 14)), title: const Text('保存全部图片', style: TextStyle(fontSize: 14)),
), ),
if (item.sourceType == SourceType.livePhoto) if (item.sourceType == .livePhoto)
ListTile( ListTile(
onTap: () { onTap: () {
Get.back(); Get.back();
@@ -592,7 +671,7 @@ class _GalleryViewerState extends State<GalleryViewer>
} }
void _showDesktopMenu(TapUpDetails details) { void _showDesktopMenu(TapUpDetails details) {
final item = widget.sources[_currIndex.value]; final item = sources[_currIndex.value];
if (item.sourceType == .fileImage) return; if (item.sourceType == .fileImage) return;
showMenu( showMenu(
context: context, context: context,
@@ -626,7 +705,7 @@ class _GalleryViewerState extends State<GalleryViewer>
onTap: () => PageUtils.launchURL(item.url), onTap: () => PageUtils.launchURL(item.url),
child: const Text('网页打开', style: TextStyle(fontSize: 14)), child: const Text('网页打开', style: TextStyle(fontSize: 14)),
), ),
if (item.sourceType == SourceType.livePhoto) if (item.sourceType == .livePhoto)
PopupMenuItem( PopupMenuItem(
height: 42, height: 42,
onTap: () => ImageUtils.downloadLivePhoto( onTap: () => ImageUtils.downloadLivePhoto(

View File

@@ -53,7 +53,7 @@ abstract final class PageUtils {
pageBuilder: (context, animation, secondaryAnimation) => GalleryViewer( pageBuilder: (context, animation, secondaryAnimation) => GalleryViewer(
sources: imgList, sources: imgList,
initIndex: initialPage, initIndex: initialPage,
quality: quality ?? GlobalData.imgQuality, thumbQ: quality ?? GlobalData.imgQuality,
onPageChanged: onPageChanged, onPageChanged: onPageChanged,
tag: tag, tag: tag,
), ),
@@ -396,7 +396,7 @@ abstract final class PageUtils {
(context) => GalleryViewer( (context) => GalleryViewer(
sources: imgList, sources: imgList,
initIndex: index, initIndex: index,
quality: GlobalData.imgQuality, thumbQ: GlobalData.imgQuality,
), ),
enableDrag: false, enableDrag: false,
elevation: 0.0, elevation: 0.0,