Co-authored-by: Felipe M. <me@fmartingr.com> Co-committed-by: Felipe M. <me@fmartingr.com>
132 lines
4.2 KiB
Dart
132 lines
4.2 KiB
Dart
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<PageSpread> spreads;
|
|
final PageController pageController;
|
|
final String Function(int pageIndex) getPageUrl;
|
|
final Map<String, String> headers;
|
|
final FilterQuality filterQuality;
|
|
final bool reverse;
|
|
final bool einkMode;
|
|
final ValueChanged<int> 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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|