// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io' show File; import 'dart:math' as math; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/image_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/semantics.dart'; class Image extends StatefulWidget { const Image({ super.key, required this.image, this.frameBuilder, this.loadingBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, this.filterQuality = FilterQuality.medium, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.tapGestureRecognizer, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }); Image.network( String src, { super.key, double scale = 1.0, this.frameBuilder, this.loadingBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.filterQuality = FilterQuality.medium, this.isAntiAlias = false, Map? headers, int? cacheWidth, int? cacheHeight, WebHtmlElementStrategy webHtmlElementStrategy = WebHtmlElementStrategy.never, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.tapGestureRecognizer, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, NetworkImage( src, scale: scale, headers: headers, webHtmlElementStrategy: webHtmlElementStrategy, ), ), assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); Image.file( File file, { super.key, double scale = 1.0, this.frameBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, this.filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.tapGestureRecognizer, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : assert( !kIsWeb, 'Image.file is not supported on Flutter Web. ' 'Consider using either Image.asset or Image.network instead.', ), image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, FileImage(file, scale: scale), ), loadingBuilder = null, assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); Image.asset( String name, { super.key, AssetBundle? bundle, this.frameBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, double? scale, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, String? package, this.filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.tapGestureRecognizer, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, scale != null ? ExactAssetImage( name, bundle: bundle, scale: scale, package: package, ) : AssetImage(name, bundle: bundle, package: package), ), loadingBuilder = null, assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); Image.memory( Uint8List bytes, { super.key, double scale = 1.0, this.frameBuilder, this.errorBuilder, this.semanticLabel, this.excludeFromSemantics = false, this.width, this.height, this.color, this.opacity, this.colorBlendMode, this.fit, this.alignment = Alignment.center, this.repeat = ImageRepeat.noRepeat, this.centerSlice, this.matchTextDirection = false, this.gaplessPlayback = false, this.isAntiAlias = false, this.filterQuality = FilterQuality.medium, int? cacheWidth, int? cacheHeight, required this.minScale, required this.maxScale, required this.containerSize, required this.onDragStart, required this.onDragUpdate, required this.onDragEnd, required this.tapGestureRecognizer, required this.doubleTapGestureRecognizer, required this.horizontalDragGestureRecognizer, required this.onChangePage, }) : image = ResizeImage.resizeIfNeeded( cacheWidth, cacheHeight, MemoryImage(bytes, scale: scale), ), loadingBuilder = null, assert(cacheWidth == null || cacheWidth > 0), assert(cacheHeight == null || cacheHeight > 0); final ImageProvider image; final ImageFrameBuilder? frameBuilder; final ImageLoadingBuilder? loadingBuilder; final ImageErrorWidgetBuilder? errorBuilder; final double? width; final double? height; final Color? color; final Animation? opacity; final FilterQuality filterQuality; final BlendMode? colorBlendMode; final BoxFit? fit; final AlignmentGeometry alignment; final ImageRepeat repeat; final Rect? centerSlice; final bool matchTextDirection; final bool gaplessPlayback; final String? semanticLabel; final bool excludeFromSemantics; final bool isAntiAlias; final double minScale; final double maxScale; final Size containerSize; final ValueChanged? onDragStart; final ValueChanged? onDragUpdate; final ValueChanged? onDragEnd; final ValueChanged? onChangePage; final ImageTapGestureRecognizer tapGestureRecognizer; final ImageDoubleTapGestureRecognizer doubleTapGestureRecognizer; final ImageHorizontalDragGestureRecognizer horizontalDragGestureRecognizer; @override State createState() => _ImageState(); @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties ..add(DiagnosticsProperty('image', image)) ..add(DiagnosticsProperty('frameBuilder', frameBuilder)) ..add( DiagnosticsProperty('loadingBuilder', loadingBuilder), ) ..add(DoubleProperty('width', width, defaultValue: null)) ..add(DoubleProperty('height', height, defaultValue: null)) ..add(ColorProperty('color', color, defaultValue: null)) ..add( DiagnosticsProperty?>( 'opacity', opacity, defaultValue: null, ), ) ..add( EnumProperty( 'colorBlendMode', colorBlendMode, defaultValue: null, ), ) ..add(EnumProperty('fit', fit, defaultValue: null)) ..add( DiagnosticsProperty( 'alignment', alignment, defaultValue: null, ), ) ..add( EnumProperty( 'repeat', repeat, defaultValue: ImageRepeat.noRepeat, ), ) ..add( DiagnosticsProperty( 'centerSlice', centerSlice, defaultValue: null, ), ) ..add( FlagProperty( 'matchTextDirection', value: matchTextDirection, ifTrue: 'match text direction', ), ) ..add( StringProperty('semanticLabel', semanticLabel, defaultValue: null), ) ..add( DiagnosticsProperty( 'this.excludeFromSemantics', excludeFromSemantics, ), ) ..add(EnumProperty('filterQuality', filterQuality)); } } class _ImageState extends State with WidgetsBindingObserver { ImageStream? _imageStream; ImageInfo? _imageInfo; ImageChunkEvent? _loadingProgress; bool _isListeningToStream = false; int? _frameNumber; bool _wasSynchronouslyLoaded = false; late DisposableBuildContext> _scrollAwareContext; Object? _lastException; StackTrace? _lastStack; ImageStreamCompleterHandle? _completerHandle; bool _isPaused = false; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _scrollAwareContext = DisposableBuildContext>(this); } @override void dispose() { assert(_imageStream != null); WidgetsBinding.instance.removeObserver(this); _stopListeningToStream(); _completerHandle?.dispose(); _scrollAwareContext.dispose(); _replaceImage(info: null); super.dispose(); } @override void didChangeDependencies() { _resolveImage(); _isPaused = !TickerMode.valuesOf(context).enabled || (MediaQuery.maybeDisableAnimationsOf(context) ?? false); if (_isPaused && _frameNumber != null) { _stopListeningToStream(keepStreamAlive: true); } else { _listenToStream(); } super.didChangeDependencies(); } @override void didUpdateWidget(Image oldWidget) { super.didUpdateWidget(oldWidget); if (_isListeningToStream && (widget.loadingBuilder == null) != (oldWidget.loadingBuilder == null)) { final ImageStreamListener oldListener = _getListener(); _imageStream!.addListener(_getListener(recreateListener: true)); _imageStream!.removeListener(oldListener); } if (widget.image != oldWidget.image) { _resolveImage(); _listenToStream(); } } @override void reassemble() { _resolveImage(); super.reassemble(); } void _resolveImage() { final provider = ScrollAwareImageProvider( context: _scrollAwareContext, imageProvider: widget.image, ); final ImageStream newStream = provider.resolve( createLocalImageConfiguration( context, size: widget.width != null && widget.height != null ? Size(widget.width!, widget.height!) : null, ), ); _updateSourceStream(newStream); } ImageStreamListener? _imageStreamListener; ImageStreamListener _getListener({bool recreateListener = false}) { if (_imageStreamListener == null || recreateListener) { _lastException = null; _lastStack = null; _imageStreamListener = ImageStreamListener( _handleImageFrame, onChunk: widget.loadingBuilder == null ? null : _handleImageChunk, onError: widget.errorBuilder != null || kDebugMode ? (Object error, StackTrace? stackTrace) { setState(() { _lastException = error; _lastStack = stackTrace; }); assert(() { if (widget.errorBuilder == null) { throw error; } return true; }()); } : null, ); } return _imageStreamListener!; } void _handleImageFrame(ImageInfo imageInfo, bool synchronousCall) { setState(() { _replaceImage(info: imageInfo); _loadingProgress = null; _lastException = null; _lastStack = null; _frameNumber = _frameNumber == null ? 0 : _frameNumber! + 1; _wasSynchronouslyLoaded = _wasSynchronouslyLoaded | synchronousCall; }); if (_isPaused) { _stopListeningToStream(keepStreamAlive: true); } } void _handleImageChunk(ImageChunkEvent event) { assert(widget.loadingBuilder != null); setState(() { _loadingProgress = event; _lastException = null; _lastStack = null; }); } void _replaceImage({required ImageInfo? info}) { final ImageInfo? oldImageInfo = _imageInfo; if (oldImageInfo != null) { SchedulerBinding.instance.addPostFrameCallback( (Duration duration) => oldImageInfo.dispose(), debugLabel: 'Image.disposeOldInfo', ); } _imageInfo = info; } void _updateSourceStream(ImageStream newStream) { if (_imageStream?.key == newStream.key) { return; } if (_isListeningToStream) { _imageStream!.removeListener(_getListener()); } if (!widget.gaplessPlayback) { setState(() { _replaceImage(info: null); }); } setState(() { _loadingProgress = null; _frameNumber = null; _wasSynchronouslyLoaded = false; }); _imageStream = newStream; if (_isListeningToStream) { _imageStream!.addListener(_getListener()); } } void _listenToStream() { if (_isListeningToStream) { return; } _isListeningToStream = true; _imageStream!.addListener(_getListener()); _completerHandle?.dispose(); _completerHandle = null; } void _stopListeningToStream({bool keepStreamAlive = false}) { if (!_isListeningToStream) { return; } if (keepStreamAlive && _completerHandle == null && _imageStream?.completer != null) { _completerHandle = _imageStream!.completer!.keepAlive(); } if (_imageStream!.completer != null && widget.errorBuilder != null) { _imageStream!.completer!.addEphemeralErrorListener( ( Object exception, StackTrace? stackTrace, ) {}, ); } _imageStream!.removeListener(_getListener()); _isListeningToStream = false; } // Widget _debugBuildErrorWidget(BuildContext context, Object error) { // return Stack( // alignment: Alignment.center, // children: [ // const Positioned.fill(child: Placeholder(color: Color(0xCF8D021F))), // Padding( // padding: const EdgeInsets.all(4.0), // child: FittedBox( // child: Text( // '$error', // textAlign: TextAlign.center, // textDirection: TextDirection.ltr, // style: const TextStyle( // shadows: [Shadow(blurRadius: 1.0)], // ), // ), // ), // ), // ], // ); // } @override Widget build(BuildContext context) { if (_lastException != null) { if (widget.errorBuilder != null) { return widget.errorBuilder!(context, _lastException!, _lastStack); } // if (kDebugMode) { // return _debugBuildErrorWidget(context, _lastException!); // } } final Size childSize; final bool isLongPic; double? minScale, maxScale; if (_imageInfo != null) { final imgWidth = _imageInfo!.image.width.toDouble(); final imgHeight = _imageInfo!.image.height.toDouble(); final imgRatio = imgHeight / imgWidth; isLongPic = imgRatio > StyleString.imgMaxRatio && imgHeight > widget.containerSize.height; if (isLongPic) { final compatWidth = math.min(650.0, widget.containerSize.width); minScale = compatWidth / widget.containerSize.height * imgRatio; maxScale = math.max(widget.maxScale, minScale * 3); } childSize = Size(imgWidth, imgHeight); } else { childSize = .zero; isLongPic = false; } Widget result = Viewer( minScale: minScale ?? widget.minScale, maxScale: maxScale ?? widget.maxScale, isLongPic: isLongPic, containerSize: widget.containerSize, childSize: childSize, onDragStart: widget.onDragStart, onDragUpdate: widget.onDragUpdate, onDragEnd: widget.onDragEnd, tapGestureRecognizer: widget.tapGestureRecognizer, doubleTapGestureRecognizer: widget.doubleTapGestureRecognizer, horizontalDragGestureRecognizer: widget.horizontalDragGestureRecognizer, onChangePage: widget.onChangePage, child: RawImage(image: _imageInfo?.image), ); if (!widget.excludeFromSemantics) { result = Semantics( container: widget.semanticLabel != null, image: true, label: widget.semanticLabel ?? '', child: result, ); } if (widget.frameBuilder != null) { result = widget.frameBuilder!( context, result, _frameNumber, _wasSynchronouslyLoaded, ); } if (widget.loadingBuilder != null) { result = widget.loadingBuilder!(context, result, _loadingProgress); } return result; } @override void debugFillProperties(DiagnosticPropertiesBuilder description) { super.debugFillProperties(description); description ..add(DiagnosticsProperty('stream', _imageStream)) ..add(DiagnosticsProperty('pixels', _imageInfo)) ..add( DiagnosticsProperty( 'loadingProgress', _loadingProgress, ), ) ..add(DiagnosticsProperty('frameNumber', _frameNumber)) ..add( DiagnosticsProperty( 'wasSynchronouslyLoaded', _wasSynchronouslyLoaded, ), ); } }