import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:photo_view/photo_view.dart'; import 'package:worldhopper/screens/reader/page_grouping.dart'; /// Renders a two-page spread reader using a PageView. /// /// Each "page" in the PageView corresponds to one [PageSpread] — either a /// single page (cover, landscape, or unpaired) or a pair of portrait pages. class TwoPageReader extends StatelessWidget { final List spreads; final PageController pageController; final String Function(int pageIndex) getPageUrl; final Map headers; final FilterQuality filterQuality; final bool reverse; final bool einkMode; final ValueChanged onPageChanged; final VoidCallback onTap; const TwoPageReader({ super.key, required this.spreads, required this.pageController, required this.getPageUrl, required this.headers, required this.filterQuality, required this.reverse, this.einkMode = false, required this.onPageChanged, required this.onTap, }); @override Widget build(BuildContext context) { return GestureDetector( onTap: onTap, child: PageView.builder( controller: pageController, reverse: reverse, itemCount: spreads.length, onPageChanged: onPageChanged, physics: einkMode ? const NeverScrollableScrollPhysics() : const BouncingScrollPhysics(), itemBuilder: (context, spreadIndex) { final spread = spreads[spreadIndex]; if (spread.isSingle) { return _buildSinglePage(spread.pages[0]); } else { return _buildPairedPages(spread.pages[0], spread.pages[1]); } }, ), ); } /// Single page: use PhotoView for full zoom/pan support. Widget _buildSinglePage(int pageIndex) { return PhotoView( imageProvider: CachedNetworkImageProvider( getPageUrl(pageIndex), headers: headers, ), filterQuality: filterQuality, minScale: PhotoViewComputedScale.contained, maxScale: PhotoViewComputedScale.covered * 3, backgroundDecoration: const BoxDecoration(color: Colors.black), loadingBuilder: (context, event) => Center( child: CircularProgressIndicator( value: event == null ? 0 : event.cumulativeBytesLoaded / (event.expectedTotalBytes ?? 1), color: Colors.white, ), ), heroAttributes: PhotoViewHeroAttributes(tag: 'page-$pageIndex'), ); } /// Paired pages: InteractiveViewer wrapping two images centered together. Widget _buildPairedPages(int leftPageIndex, int rightPageIndex) { return LayoutBuilder( builder: (context, constraints) { final halfWidth = constraints.maxWidth / 2; final maxHeight = constraints.maxHeight; return InteractiveViewer( minScale: 1.0, maxScale: 3.0, child: SizedBox( width: constraints.maxWidth, height: maxHeight, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ _buildHalfPage( leftPageIndex, halfWidth, maxHeight, Alignment.centerRight), _buildHalfPage( rightPageIndex, halfWidth, maxHeight, Alignment.centerLeft), ], ), ), ); }, ); } Widget _buildHalfPage( int pageIndex, double width, double height, Alignment alignment) { return SizedBox( width: width, height: height, child: CachedNetworkImage( imageUrl: getPageUrl(pageIndex), httpHeaders: headers, filterQuality: filterQuality, fit: BoxFit.contain, alignment: alignment, placeholder: (context, url) => const Center( child: CircularProgressIndicator(color: Colors.white), ), errorWidget: (context, url, error) => const Center( child: Icon(Icons.error, color: Colors.white), ), ), ); } }