import 'dart:async'; import 'dart:io'; import 'package:another_xlider/another_xlider.dart'; import 'package:event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_styled_toast/flutter_styled_toast.dart'; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:pikapika/basic/Common.dart'; import 'package:pikapika/basic/Cross.dart'; import 'package:pikapika/basic/Entities.dart'; import 'package:pikapika/basic/Method.dart'; import 'package:pikapika/basic/config/Address.dart'; import 'package:pikapika/basic/config/FullScreenAction.dart'; import 'package:pikapika/basic/config/ImageAddress.dart'; import 'package:pikapika/basic/config/ImageFilter.dart'; import 'package:pikapika/basic/config/KeyboardController.dart'; import 'package:pikapika/basic/config/NoAnimation.dart'; import 'package:pikapika/basic/config/Quality.dart'; import 'package:pikapika/basic/config/ReaderDirection.dart'; import 'package:pikapika/basic/config/ReaderSliderPosition.dart'; import 'package:pikapika/basic/config/ReaderType.dart'; import 'package:pikapika/basic/config/VolumeController.dart'; import 'package:pikapika/screens/components/PkzImages.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import '../../basic/config/IconLoading.dart'; import '../../basic/config/ReaderBackgroundColor.dart'; import '../../basic/config/UseApiLoadImage.dart'; import '../../basic/config/VolumeNextChapter.dart'; import '../FilePhotoViewScreen.dart'; import 'gesture_zoom_box.dart'; import 'Images.dart'; /////////////// Event<_ReaderControllerEventArgs> _readerControllerEvent = Event<_ReaderControllerEventArgs>(); class _ReaderControllerEventArgs extends EventArgs { final String key; _ReaderControllerEventArgs(this.key); } Widget readerKeyboardHolder(Widget widget) { if (keyboardController && (Platform.isWindows || Platform.isMacOS || Platform.isLinux)) { widget = RawKeyboardListener( focusNode: FocusNode(), child: widget, autofocus: true, onKey: (event) { if (event is RawKeyDownEvent) { if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) { _readerControllerEvent.broadcast(_ReaderControllerEventArgs("UP")); } if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) { _readerControllerEvent .broadcast(_ReaderControllerEventArgs("DOWN")); } } }, ); } return widget; } void _onVolumeEvent(dynamic args) { _readerControllerEvent.broadcast(_ReaderControllerEventArgs("$args")); } var _volumeListenCount = 0; // 仅支持安卓 // 监听后会拦截安卓手机音量键 // 仅最后一次监听生效 // event可能为DOWN/UP EventChannel volumeButtonChannel = const EventChannel("volume_button"); StreamSubscription? volumeS; void addVolumeListen() { _volumeListenCount++; if (_volumeListenCount == 1) { volumeS = volumeButtonChannel.receiveBroadcastStream().listen(_onVolumeEvent); } } void delVolumeListen() { _volumeListenCount--; if (_volumeListenCount == 0) { volumeS?.cancel(); } } /////////////////////////////////////////////////////////////////////////////// // 对Reader的传参以及封装 class PkzFile { final String pkzPath; final String path; PkzFile(this.pkzPath, this.path); } class ReaderImageInfo { final String fileServer; final String path; final String? downloadLocalPath; final int? width; final int? height; final String? format; final int? fileSize; final PkzFile? pkzFile; ReaderImageInfo( this.fileServer, this.path, this.downloadLocalPath, this.width, this.height, this.format, this.fileSize, { this.pkzFile, }); } class ImageReaderStruct { final List images; final bool fullScreen; final FutureOr Function(bool fullScreen) onFullScreenChange; final FutureOr Function(int) onPositionChange; final int? initPosition; final Map epNameMap; final int epOrder; final String comicTitle; final FutureOr Function(int) onChangeEp; final FutureOr Function() onReloadEp; final FutureOr Function() onDownload; const ImageReaderStruct({ required this.images, required this.fullScreen, required this.onFullScreenChange, required this.onPositionChange, this.initPosition, required this.epNameMap, required this.epOrder, required this.comicTitle, required this.onChangeEp, required this.onReloadEp, required this.onDownload, }); } // class ImageReader extends StatefulWidget { final ImageReaderStruct struct; const ImageReader(this.struct, {Key? key}) : super(key: key); @override State createState() => _ImageReaderState(); } class _ImageReaderState extends State { // 记录初始方向 final ReaderDirection _pagerDirection = gReaderDirection; // 记录初始阅读器类型 final ReaderType _pagerType = currentReaderType(); // 记录了控制器 late final FullScreenAction _fullScreenAction = currentFullScreenAction(); late final ReaderSliderPosition _readerSliderPosition = currentReaderSliderPosition(); @override Widget build(BuildContext context) { return _ImageReaderContent( widget.struct, _pagerDirection, _pagerType, _fullScreenAction, _readerSliderPosition, ); } } // class _ImageReaderContent extends StatefulWidget { // 记录初始方向 final ReaderDirection pagerDirection; // 记录初始阅读器类型 final ReaderType pagerType; final FullScreenAction fullScreenAction; final ReaderSliderPosition readerSliderPosition; final ImageReaderStruct struct; const _ImageReaderContent(this.struct, this.pagerDirection, this.pagerType, this.fullScreenAction, this.readerSliderPosition); @override State createState() { switch (pagerType) { case ReaderType.WEB_TOON: return _WebToonReaderState(); case ReaderType.WEB_TOON_ZOOM: return _WebToonZoomReaderState(); case ReaderType.GALLERY: return _GalleryReaderState(); case ReaderType.WEB_TOON_FREE_ZOOM: return _ListViewReaderState(); default: throw Exception("ERROR READER TYPE"); } } } abstract class _ImageReaderContentState extends State<_ImageReaderContent> { // 阅读器 Widget _buildViewer(); Widget _buildViewerProcess() => processImageFilter(_buildViewer()); // 键盘, 音量键 等事件 void _needJumpTo(int index, bool animation); // 记录了是否切换了音量 late bool _listVolume; // 和初始化与翻页有关 @override void initState() { _initCurrent(); _readerControllerEvent.subscribe(_onPageControl); _listVolume = volumeController; if (_listVolume) { addVolumeListen(); } super.initState(); } @override void dispose() { _readerControllerEvent.unsubscribe(_onPageControl); if (_listVolume) { delVolumeListen(); } super.dispose(); } void _onPageControl(_ReaderControllerEventArgs? args) { if (args != null) { var event = args.key; switch (event) { case "UP": if (_current > 0) { _needJumpTo(_current - 1, true); } break; case "DOWN": if (_current < widget.struct.images.length - 1) { _needJumpTo(_current + 1, true); } else { if (volumeNextChapter()) { final now = DateTime.now().millisecondsSinceEpoch; if (_noticeTime + 3000 > now) { if (_hasNextEp()) { _onNextAction(); } else { showToast( "已经到头了", context: context, position: StyledToastPosition.center, animation: StyledToastAnimation.scale, reverseAnimation: StyledToastAnimation.fade, duration: const Duration(seconds: 3), animDuration: const Duration(milliseconds: 300), curve: Curves.elasticOut, reverseCurve: Curves.linear, ); } } else { _noticeTime = now; showToast( "再次点击跳转到下一章", context: context, position: StyledToastPosition.center, animation: StyledToastAnimation.scale, reverseAnimation: StyledToastAnimation.fade, duration: const Duration(seconds: 3), animDuration: const Duration(milliseconds: 300), curve: Curves.elasticOut, reverseCurve: Curves.linear, ); } } } break; } } } int _noticeTime = 0; late int _startIndex; late int _current; late int _slider; void _initCurrent() { if (widget.struct.initPosition != null && widget.struct.images.length > widget.struct.initPosition!) { _startIndex = widget.struct.initPosition!; } else { _startIndex = 0; } _current = _startIndex; _slider = _startIndex; } void _onCurrentChange(int index) { if (index != _current) { setState(() { _current = index; _slider = index; widget.struct.onPositionChange(index); }); } } // 与显示有关的方法 @override Widget build(BuildContext context) { switch (currentFullScreenAction()) { // 按钮 case FullScreenAction.CONTROLLER: return Stack( children: [ _buildViewerProcess(), _buildBar(_buildFullScreenControllerStackItem()), ], ); case FullScreenAction.TOUCH_ONCE: return Stack( children: [ _buildTouchOnceControllerAction(_buildViewerProcess()), _buildBar(Container()), ], ); case FullScreenAction.TOUCH_DOUBLE: return Stack( children: [ _buildTouchDoubleControllerAction(_buildViewerProcess()), _buildBar(Container()), ], ); case FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT: return Stack( children: [ _buildTouchDoubleOnceNextControllerAction(_buildViewerProcess()), _buildBar(Container()), ], ); case FullScreenAction.THREE_AREA: return Stack( children: [ _buildViewerProcess(), _buildBar(_buildThreeAreaControllerAction()), ], ); } } Widget _buildBar(Widget child) { switch (widget.readerSliderPosition) { case ReaderSliderPosition.BOTTOM: return Column( children: [ _buildAppBar(), Expanded(child: child), widget.struct.fullScreen ? Container() : Container( height: 45, color: const Color(0x88000000), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Container(width: 15), IconButton( icon: const Icon(Icons.fullscreen), color: Colors.white, onPressed: () { widget.struct .onFullScreenChange(!widget.struct.fullScreen); }, ), Container(width: 10), Expanded( child: widget.pagerType != ReaderType.WEB_TOON_FREE_ZOOM ? _buildSliderBottom() : Container(), ), Container(width: 10), IconButton( icon: const Icon(Icons.skip_next_outlined), color: Colors.white, onPressed: _onNextAction, ), Container(width: 15), ], ), ), widget.struct.fullScreen ? Container() : Container( color: const Color(0x88000000), child: SafeArea( top: false, child: Container(), ), ), ], ); case ReaderSliderPosition.RIGHT: return Column( children: [ _buildAppBar(), Expanded( child: Stack( children: [ child, _buildSliderRight(), ], ), ), ], ); case ReaderSliderPosition.LEFT: return Column( children: [ _buildAppBar(), Expanded( child: Stack( children: [ child, _buildSliderLeft(), ], ), ), ], ); } } Widget _buildAppBar() => widget.struct.fullScreen ? Container() : AppBar( title: Text( "${widget.struct.epNameMap[widget.struct.epOrder] ?? ""} - ${widget.struct.comicTitle}"), actions: [ IconButton( onPressed: _onChooseEp, icon: const Icon(Icons.menu_open), ), IconButton( onPressed: _onMoreSetting, icon: const Icon(Icons.more_horiz), ), ], ); Widget _buildSliderBottom() { return Column( children: [ Expanded(child: Container()), SizedBox( height: 25, child: _buildSliderWidget(Axis.horizontal), ), Expanded(child: Container()), ], ); } Widget _buildSliderLeft() => widget.struct.fullScreen ? Container() : Align( alignment: Alignment.centerLeft, child: Material( color: Colors.transparent, child: Container( width: 35, height: 300, decoration: const BoxDecoration( color: Color(0x66000000), borderRadius: BorderRadius.only( topRight: Radius.circular(10), bottomRight: Radius.circular(10), ), ), padding: const EdgeInsets.only(top: 10, bottom: 10, left: 6, right: 5), child: Center( child: _buildSliderWidget(Axis.vertical), ), ), ), ); Widget _buildSliderRight() => widget.struct.fullScreen ? Container() : Align( alignment: Alignment.centerRight, child: Material( color: Colors.transparent, child: Container( width: 35, height: 300, decoration: const BoxDecoration( color: Color(0x66000000), borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), ), padding: const EdgeInsets.only(top: 10, bottom: 10, left: 5, right: 6), child: Center( child: _buildSliderWidget(Axis.vertical), ), ), ), ); Widget _buildSliderWidget(Axis axis) { return FlutterSlider( axis: axis, values: [_slider.toDouble()], min: 0, max: (widget.struct.images.length - 1).toDouble(), onDragging: (handlerIndex, lowerValue, upperValue) { _slider = (lowerValue.toInt()); }, onDragCompleted: (handlerIndex, lowerValue, upperValue) { _slider = (lowerValue.toInt()); if (_slider != _current) { _needJumpTo(_slider, false); } }, trackBar: FlutterSliderTrackBar( inactiveTrackBar: BoxDecoration( borderRadius: BorderRadius.circular(20), color: Colors.grey.shade300, ), activeTrackBar: BoxDecoration( borderRadius: BorderRadius.circular(4), color: Theme.of(context).colorScheme.secondary, ), ), step: const FlutterSliderStep( step: 1, isPercentRange: false, ), tooltip: FlutterSliderTooltip(custom: (value) { double a = value + 1; return Container( padding: const EdgeInsets.all(8), decoration: ShapeDecoration( color: Colors.black.withAlpha(0xCC), shape: RoundedRectangleBorder( borderRadius: BorderRadiusDirectional.circular(3)), ), child: Text( '${a.toInt()}', style: const TextStyle( color: Colors.white, fontSize: 18, ), ), ); }), ); } Widget _buildFullScreenControllerStackItem() { if (widget.readerSliderPosition == ReaderSliderPosition.BOTTOM && !widget.struct.fullScreen) { return Container(); } if (widget.readerSliderPosition == ReaderSliderPosition.RIGHT) { return SafeArea( child: Align( alignment: Alignment.bottomRight, child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), margin: const EdgeInsets.only(bottom: 10), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: Icon( widget.struct.fullScreen ? Icons.fullscreen_exit : Icons.fullscreen_outlined, size: 30, color: Colors.white, ), ), ), ), )); } return SafeArea( child: Align( alignment: Alignment.bottomLeft, child: Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), margin: const EdgeInsets.only(bottom: 10), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topRight: Radius.circular(10), bottomRight: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: Icon( widget.struct.fullScreen ? Icons.fullscreen_exit : Icons.fullscreen_outlined, size: 30, color: Colors.white, ), ), ), ), )); } Widget _buildTouchOnceControllerAction(Widget child) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: child, ); } Widget _buildTouchDoubleControllerAction(Widget child) { return GestureDetector( behavior: HitTestBehavior.translucent, onDoubleTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: child, ); } Widget _buildTouchDoubleOnceNextControllerAction(Widget child) { return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _readerControllerEvent.broadcast(_ReaderControllerEventArgs("DOWN")); }, onDoubleTap: () { widget.struct.onFullScreenChange(!widget.struct.fullScreen); }, child: child, ); } Widget _buildThreeAreaControllerAction() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { var up = Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _readerControllerEvent .broadcast(_ReaderControllerEventArgs("UP")); }, child: Container(), ), ); var down = Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _readerControllerEvent .broadcast(_ReaderControllerEventArgs("DOWN")); }, child: Container(), ), ); var fullScreen = Expanded( child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => widget.struct.onFullScreenChange(!widget.struct.fullScreen), child: Container(), ), ); late Widget child; switch (widget.pagerDirection) { case ReaderDirection.TOP_TO_BOTTOM: child = Column(children: [ up, fullScreen, down, ]); break; case ReaderDirection.LEFT_TO_RIGHT: child = Row(children: [ up, fullScreen, down, ]); break; case ReaderDirection.RIGHT_TO_LEFT: child = Row(children: [ down, fullScreen, up, ]); break; } return SizedBox( width: constraints.maxWidth, height: constraints.maxHeight, child: child, ); }, ); } Future _onChooseEp() async { showMaterialModalBottomSheet( context: context, backgroundColor: const Color(0xAA000000), builder: (context) { return SizedBox( height: MediaQuery.of(context).size.height * (.45), child: _EpChooser( widget.struct.epNameMap, widget.struct.epOrder, widget.struct.onChangeEp, ), ); }, ); } Future _onMoreSetting() async { // 记录开始的画质 final currentQuality = currentQualityCode(); final cReaderSliderPosition = currentReaderSliderPosition(); // await showMaterialModalBottomSheet( context: context, backgroundColor: const Color(0xAA000000), builder: (context) { return SizedBox( height: MediaQuery.of(context).size.height * (.45), child: _SettingPanel( widget.struct.onReloadEp, widget.struct.onDownload, ), ); }, ); if (widget.pagerDirection != gReaderDirection || widget.pagerType != currentReaderType() || currentQuality != currentQualityCode() || widget.fullScreenAction != currentFullScreenAction() || cReaderSliderPosition != currentReaderSliderPosition()) { widget.struct.onReloadEp(); } } // 给子类调用的方法 bool _fullscreenController() { switch (currentFullScreenAction()) { case FullScreenAction.CONTROLLER: return false; case FullScreenAction.TOUCH_ONCE: return false; case FullScreenAction.TOUCH_DOUBLE: return false; case FullScreenAction.TOUCH_DOUBLE_ONCE_NEXT: return false; case FullScreenAction.THREE_AREA: return true; } } Future _onNextAction() async { if (widget.struct.epNameMap.containsKey(widget.struct.epOrder + 1)) { widget.struct.onChangeEp(widget.struct.epOrder + 1); } else { defaultToast(context, "已经到头了"); } } bool _hasNextEp() => widget.struct.epNameMap.containsKey(widget.struct.epOrder + 1); double _topBarHeight() => Scaffold.of(context).appBarMaxHeight ?? 0; double _bottomBarHeight() => widget.readerSliderPosition == ReaderSliderPosition.BOTTOM ? 45 : 0; } class _EpChooser extends StatefulWidget { final Map epNameMap; final int epOrder; final FutureOr Function(int) onChangeEp; _EpChooser(this.epNameMap, this.epOrder, this.onChangeEp); @override State createState() => _EpChooserState(); } class _EpChooserState extends State<_EpChooser> { @override Widget build(BuildContext context) { var entries = widget.epNameMap.entries.toList(); entries.sort((a, b) => a.key - b.key); var widgets = [ Container(height: 20), ...entries.map((e) { return Container( margin: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5), decoration: BoxDecoration( color: widget.epOrder == e.key ? Colors.grey.withAlpha(100) : null, border: Border.all( color: const Color(0xff484c60), style: BorderStyle.solid, width: .5, ), ), child: MaterialButton( onPressed: () { Navigator.of(context).pop(); widget.onChangeEp(e.key); }, textColor: Colors.white, child: Text(e.value), ), ); }) ]; return ScrollablePositionedList.builder( initialScrollIndex: widget.epOrder < 2 ? 0 : widget.epOrder - 2, itemCount: widgets.length, itemBuilder: (BuildContext context, int index) => widgets[index], ); } } class _SettingPanel extends StatefulWidget { final FutureOr Function() onReloadEp; final FutureOr Function() onDownload; _SettingPanel(this.onReloadEp, this.onDownload); @override State createState() => _SettingPanelState(); } class _SettingPanelState extends State<_SettingPanel> { @override Widget build(BuildContext context) { return ListView( children: [ Row( children: [ _bottomIcon( icon: Icons.crop_sharp, title: gReaderDirectionName(), onPressed: () async { await choosePagerDirection(context); setState(() {}); }, ), _bottomIcon( icon: Icons.view_day_outlined, title: currentReaderTypeName(), onPressed: () async { await choosePagerType(context); setState(() {}); }, ), _bottomIcon( icon: Icons.image_aspect_ratio_outlined, title: currentQualityName(), onPressed: () async { await chooseQuality(context); setState(() {}); }, ), _bottomIcon( icon: Icons.control_camera_outlined, title: currentFullScreenActionName(), onPressed: () async { await chooseFullScreenAction(context); setState(() {}); }, ), ], ), Row( children: [ _bottomIcon( icon: Icons.share, title: currentAddressName(), onPressed: () async { await chooseAddressAndSwitch(context); setState(() {}); }, ), _bottomIcon( icon: Icons.image_search, title: currentImageAddressName(), onPressed: () async { await chooseImageAddress(context); setState(() {}); }, ), _bottomIcon( icon: Icons.network_ping, title: currentUseApiLoadImageName(), onPressed: () { chooseUseApiLoadImage(context); }, ), _bottomIcon( icon: Icons.refresh, title: "重载页面", onPressed: () { Navigator.of(context).pop(); widget.onReloadEp(); }, ), ], ), // Row(children: [ // _bottomIcon( // icon: Icons.file_download, // title: "下载本作", // onPressed: widget.onDownload, // ), // ]), ], ); } Widget _bottomIcon({ required IconData icon, required String title, required void Function() onPressed, }) { return Expanded( child: Center( child: Column( children: [ IconButton( iconSize: 55, icon: Column( children: [ Container(height: 3), Icon( icon, size: 25, color: Colors.white, ), Container(height: 3), Text( title, style: const TextStyle(color: Colors.white, fontSize: 10), maxLines: 1, textAlign: TextAlign.center, ), Container(height: 3), ], ), onPressed: onPressed, ) ], ), ), ); } } /////////////////////////////////////////////////////////////////////////////// class _WebToonReaderState extends _ImageReaderContentState { var _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; late final List _trueSizes = []; late final ItemScrollController _itemScrollController; late final ItemPositionsListener _itemPositionsListener; @override void initState() { for (var e in widget.struct.images) { if (e.pkzFile != null && e.width != null && e.height != null && e.width! > 0 && e.height! > 0) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); } } _itemScrollController = ItemScrollController(); _itemPositionsListener = ItemPositionsListener.create(); _itemPositionsListener.itemPositions.addListener(_onListCurrentChange); super.initState(); } @override void dispose() { _itemPositionsListener.itemPositions.removeListener(_onListCurrentChange); super.dispose(); } void _onListCurrentChange() { var to = _itemPositionsListener.itemPositions.value.first.index; // 包含一个下一章, 假设5张图片 0,1,2,3,4 length=5, 下一章=5 if (to >= 0 && to < widget.struct.images.length) { super._onCurrentChange(to); } } @override void _needJumpTo(int index, bool animation) { if (noAnimation() || animation == false) { _itemScrollController.jumpTo( index: index, ); } else { if (DateTime.now().millisecondsSinceEpoch < _controllerTime) { return; } _controllerTime = DateTime.now().millisecondsSinceEpoch + 400; _itemScrollController.scrollTo( index: index, duration: const Duration(milliseconds: 400), ); } } @override Widget _buildViewer() { return Container( decoration: BoxDecoration( color: readerBackgroundColorObj, ), child: _buildList(), ); } Widget _buildList() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // reload _images size List _images = []; for (var index = 0; index < widget.struct.images.length; index++) { late Size renderSize; if (_trueSizes[index] != null) { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size( constraints.maxWidth, constraints.maxWidth * _trueSizes[index]!.height / _trueSizes[index]!.width, ); } else { var maxHeight = constraints.maxHeight - super._topBarHeight() - super._bottomBarHeight() - MediaQuery.of(context).padding.bottom; renderSize = Size( maxHeight * _trueSizes[index]!.width / _trueSizes[index]!.height, maxHeight, ); } } else { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2); } else { // ReaderDirection.LEFT_TO_RIGHT // ReaderDirection.RIGHT_TO_LEFT renderSize = Size(constraints.maxWidth / 2, constraints.maxHeight); } } var currentIndex = index; onTrueSize(Size size) { setState(() { _trueSizes[currentIndex] = size; }); } var e = widget.struct.images[index]; if (e.pkzFile != null) { _images.add(_WebToonPkzImage( width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, pkzFile: e.pkzFile!, )); } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, localPath: e.downloadLocalPath!, fileSize: e.fileSize!, width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, )); } else { _images.add(_WebToonRemoteImage( e.fileServer, e.path, renderSize, onTrueSize, )); } } return ScrollablePositionedList.builder( initialScrollIndex: super._startIndex, scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( // 不管全屏与否, 滚动方向如何, 顶部永远保持间距 top: super._topBarHeight(), bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 // 纵向滚动 底部永远都是130的空白 : (super._bottomBarHeight() + MediaQuery.of(context).padding.bottom) // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果 , ), itemScrollController: _itemScrollController, itemPositionsListener: _itemPositionsListener, itemCount: widget.struct.images.length + 1, itemBuilder: (BuildContext context, int index) { if (widget.struct.images.length == index) { return _buildNextEp(); } return _images[index]; }, ); }, ); } Widget _buildNextEp() { if (super._fullscreenController()) { return Container(); } return Container( color: Colors.transparent, padding: const EdgeInsets.all(20), child: MaterialButton( onPressed: () { if (super._hasNextEp()) { super._onNextAction(); } else { Navigator.of(context).pop(); } }, textColor: invertColor(readerBackgroundColorObj), child: Container( padding: const EdgeInsets.only(top: 40, bottom: 40), child: Text(super._hasNextEp() ? '下一章' : '结束阅读'), ), ), ); } } // 来自下载 class _WebToonDownloadImage extends _WebToonReaderImage { final String fileServer; final String path; final String localPath; final int fileSize; final int width; final int height; final String format; _WebToonDownloadImage({ required this.fileServer, required this.path, required this.localPath, required this.fileSize, required this.width, required this.height, required this.format, required Size size, Function(Size)? onTrueSize, }) : super(size, onTrueSize); @override Future imageData() async { if (localPath == "") { return method.remoteImageData(fileServer, path); } var finalPath = await method.downloadImagePath(localPath); return RemoteImageData.forData( fileSize, format, width, height, finalPath, ); } } // 来自PKZ class _WebToonPkzImage extends StatelessWidget { final PkzFile pkzFile; final int width; final int height; final String format; final Size size; Function(Size)? onTrueSize; _WebToonPkzImage({ required this.pkzFile, required this.width, required this.height, required this.format, required this.size, required this.onTrueSize, }); @override Widget build(BuildContext context) { return PkzLoadingImage( pkzPath: pkzFile.pkzPath, path: pkzFile.path, width: size.width, height: size.height, onTrueSize: onTrueSize, ); } } // 来自远端 class _WebToonRemoteImage extends _WebToonReaderImage { final String fileServer; final String path; _WebToonRemoteImage( this.fileServer, this.path, Size size, Function(Size)? onTrueSize, ) : super(size, onTrueSize); @override Future imageData() async { return method.remoteImageData(fileServer, path); } } // 通用 abstract class _WebToonReaderImage extends StatefulWidget { final Size size; final Function(Size)? onTrueSize; _WebToonReaderImage(this.size, this.onTrueSize); @override State createState() => _WebToonReaderImageState(); Future imageData(); } class _WebToonReaderImageState extends State<_WebToonReaderImage> { late Future _future = _load(); Future _load() { return widget.imageData().then((value) { widget.onTrueSize?.call( Size(value.width.toDouble(), value.height.toDouble()), ); return value; }); } @override Widget build(BuildContext context) { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return FutureBuilder( future: _future, builder: ( BuildContext context, AsyncSnapshot snapshot, ) { if (snapshot.hasError) { return GestureDetector( onLongPress: () async { String? choose = await chooseListDialog(context, '请选择', ['重新加载图片']); switch (choose) { case '重新加载图片': setState(() { _future = _load(); }); break; } }, child: buildError(widget.size.width, widget.size.height), ); } if (snapshot.connectionState != ConnectionState.done) { return buildLoading(widget.size.width, widget.size.height); } var data = snapshot.data!; return buildFile( data.finalPath, widget.size.width, widget.size.height, context: context, ); }, ); }, ); } } /////////////////////////////////////////////////////////////////////////////// class _WebToonZoomReaderState extends _WebToonReaderState { @override Widget _buildList() { return GestureZoomBox(child: super._buildList()); } } /////////////////////////////////////////////////////////////////////////////// class _ListViewReaderState extends _ImageReaderContentState with SingleTickerProviderStateMixin { final List _trueSizes = []; final _transformationController = TransformationController(); late TapDownDetails _doubleTapDetails; late final _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); @override void initState() { for (var e in widget.struct.images) { if (e.pkzFile != null && e.width != null && e.height != null && e.width! > 0 && e.height! > 0) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else if (e.downloadLocalPath != null) { _trueSizes.add(Size(e.width!.toDouble(), e.height!.toDouble())); } else { _trueSizes.add(null); } } super.initState(); } @override void dispose() { _transformationController.dispose(); _animationController.dispose(); super.dispose(); } @override void _needJumpTo(int index, bool animation) {} @override Widget _buildViewer() { return Container( decoration: BoxDecoration( color: readerBackgroundColorObj, ), child: _buildList(), ); } Widget _buildList() { return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { // reload _images size List _images = []; for (var index = 0; index < widget.struct.images.length; index++) { late Size renderSize; if (_trueSizes[index] != null) { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size( constraints.maxWidth, constraints.maxWidth * _trueSizes[index]!.height / _trueSizes[index]!.width, ); } else { var maxHeight = constraints.maxHeight - super._topBarHeight() - super._bottomBarHeight() - MediaQuery.of(context).padding.bottom; renderSize = Size( maxHeight * _trueSizes[index]!.width / _trueSizes[index]!.height, maxHeight, ); } } else { if (widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM) { renderSize = Size(constraints.maxWidth, constraints.maxWidth / 2); } else { // ReaderDirection.LEFT_TO_RIGHT // ReaderDirection.RIGHT_TO_LEFT renderSize = Size(constraints.maxWidth / 2, constraints.maxHeight); } } var currentIndex = index; onTrueSize(Size size) { setState(() { _trueSizes[currentIndex] = size; }); } var e = widget.struct.images[index]; if (e.pkzFile != null) { _images.add(_WebToonPkzImage( width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, pkzFile: e.pkzFile!, )); } else if (e.downloadLocalPath != null) { _images.add(_WebToonDownloadImage( fileServer: e.fileServer, path: e.path, localPath: e.downloadLocalPath!, fileSize: e.fileSize!, width: e.width!, height: e.height!, format: e.format!, size: renderSize, onTrueSize: onTrueSize, )); } else { _images.add(_WebToonRemoteImage( e.fileServer, e.path, renderSize, onTrueSize, )); } } var list = ListView.builder( scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, padding: EdgeInsets.only( // 不管全屏与否, 滚动方向如何, 顶部永远保持间距 top: super._topBarHeight(), bottom: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? 130 // 纵向滚动 底部永远都是130的空白 : (super._bottomBarHeight() + MediaQuery.of(context).padding.bottom) // 非全屏时, 顶部去掉顶部BAR的高度, 底部去掉底部BAR的高度, 形成看似填充的效果 , ), itemCount: widget.struct.images.length + 1, itemBuilder: (BuildContext context, int index) { if (widget.struct.images.length == index) { return _buildNextEp(); } return _images[index]; }, ); var viewer = InteractiveViewer( transformationController: _transformationController, minScale: 1, maxScale: 2, child: list, ); return GestureDetector( onDoubleTap: _handleDoubleTap, onDoubleTapDown: _handleDoubleTapDown, child: viewer, ); }, ); } Widget _buildNextEp() { if (super._fullscreenController()) { return Container(); } return Container( padding: const EdgeInsets.all(20), child: MaterialButton( onPressed: () { if (super._hasNextEp()) { super._onNextAction(); } else { Navigator.of(context).pop(); } }, textColor: invertColor(readerBackgroundColorObj), child: Container( padding: const EdgeInsets.only(top: 40, bottom: 40), child: Text(super._hasNextEp() ? '下一章' : '结束阅读'), ), ), ); } void _handleDoubleTapDown(TapDownDetails details) { _doubleTapDetails = details; } void _handleDoubleTap() { if (_animationController.isAnimating) { return; } if (_transformationController.value != Matrix4.identity()) { _transformationController.value = Matrix4.identity(); } else { var position = _doubleTapDetails.localPosition; var animation = Tween(begin: 0, end: 1.0).animate(_animationController); animation.addListener(() { _transformationController.value = Matrix4.identity() ..translate( -position.dx * animation.value, -position.dy * animation.value) ..scale(animation.value + 1.0); }); _animationController.forward(from: 0); } } } /////////////////////////////////////////////////////////////////////////////// class _GalleryReaderState extends _ImageReaderContentState { late PageController _pageController; @override void initState() { super.initState(); // 需要先初始化 super._startIndex 才能使用, 所以在上面 _pageController = PageController(initialPage: super._startIndex); } @override void dispose() { _pageController.dispose(); super.dispose(); } @override void _needJumpTo(int index, bool animation) { if (noAnimation() || animation == false) { _pageController.jumpToPage( index, ); } else { _pageController.animateToPage( index, duration: const Duration(milliseconds: 400), curve: Curves.ease, ); } } void _onGalleryPageChange(int to) { // 包含一个下一章, 假设5张图片 0,1,2,3,4 length=5, 下一章=5 if (to >= 0 && to < widget.struct.images.length) { super._onCurrentChange(to); } } @override Widget _buildViewer() { Widget gallery = PhotoViewGallery.builder( scrollDirection: widget.pagerDirection == ReaderDirection.TOP_TO_BOTTOM ? Axis.vertical : Axis.horizontal, reverse: widget.pagerDirection == ReaderDirection.RIGHT_TO_LEFT, backgroundDecoration: BoxDecoration(color: readerBackgroundColorObj), loadingBuilder: (context, event) => LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return buildLoading(constraints.maxWidth, constraints.maxHeight); }, ), pageController: _pageController, onPageChanged: _onGalleryPageChange, itemCount: widget.struct.images.length, builder: (BuildContext context, int index) { var item = widget.struct.images[index]; if (item.pkzFile != null) { return PhotoViewGalleryPageOptions( imageProvider: PkzImageProvider(item.pkzFile!.pkzPath, item.pkzFile!.path), errorBuilder: (b, e, s) { print("$e,$s"); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return buildError( constraints.maxWidth, constraints.maxHeight); }, ); }, filterQuality: FilterQuality.high, ); } if (item.downloadLocalPath != null) { return PhotoViewGalleryPageOptions( imageProvider: ResourceDownloadFileImageProvider(item.downloadLocalPath!), errorBuilder: (b, e, s) { print("$e,$s"); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return buildError( constraints.maxWidth, constraints.maxHeight); }, ); }, filterQuality: FilterQuality.high, ); } return PhotoViewGalleryPageOptions( imageProvider: ResourceRemoteImageProvider(item.fileServer, item.path), errorBuilder: (b, e, s) { print("$e,$s"); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return buildError(constraints.maxWidth, constraints.maxHeight); }, ); }, filterQuality: FilterQuality.high, ); }, allowImplicitScrolling: true, ); gallery = GestureDetector( child: gallery, onLongPress: () async { if (_current >= 0 && _current < widget.struct.images.length) { var item = widget.struct.images[_current]; if (item.pkzFile != null) { return; } Future load() async { var item = widget.struct.images[_current]; if (item.downloadLocalPath != null) { return method.downloadImagePath(item.downloadLocalPath!); } var data = await method.remoteImageData(item.fileServer, item.path); return data.finalPath; } String? choose = await chooseListDialog(context, '请选择', ['预览图片', '保存图片']); switch (choose) { case '预览图片': try { var file = await load(); Navigator.of(context).push(mixRoute( builder: (context) => FilePhotoViewScreen(file), )); } catch (e) { defaultToast(context, "图片加载失败"); } break; case '保存图片': try { var file = await load(); saveImage(file, context); } catch (e) { defaultToast(context, "图片加载失败"); } break; } } }, ); gallery = Container( padding: EdgeInsets.only( top: widget.struct.fullScreen ? 0 : super._topBarHeight(), bottom: widget.struct.fullScreen ? 0 : super._bottomBarHeight(), ), child: gallery, ); return Stack( children: [ gallery, _buildNextEpController(), ], ); } Widget _buildNextEpController() { if (super._fullscreenController() || _current < widget.struct.images.length - 1) return Container(); return Align( alignment: Alignment.bottomRight, child: Material( color: Colors.transparent, child: Container( margin: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(left: 10, right: 10, top: 4, bottom: 4), decoration: const BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(10), bottomLeft: Radius.circular(10), ), color: Color(0x88000000), ), child: GestureDetector( onTap: () { if (_hasNextEp()) { _onNextAction(); } else { Navigator.of(context).pop(); } }, child: Text( _hasNextEp() ? '下一章' : '结束阅读', style: const TextStyle(color: Colors.white), ), ), ), ), ); } } Color invertColor(Color color) { return Color.fromRGBO( 255 - color.red, 255 - color.green, 255 - color.blue, 1.0, ); } ///////////////////////////////////////////////////////////////////////////////