mangayomi-mirror/lib/modules/manga/reader/image_view_webtoon.dart
Moustapha Kodjo Amadou 2f0fc85316 feat(reader): add navigation layout options and settings for screen behavior
- Implemented multiple navigation layouts for the reader, including L-shaped, Kindle, Edge, Right & Left, and Disabled modes.
- Added settings for keeping the screen on, showing page gaps, and adjusting webtoon side padding.
- Enhanced the reader settings modal to include new options and improved UI for navigation layout selection.
- Color filters: invert/gray/BCS
2026-03-04 16:04:07 +01:00

172 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/widgets/double_page_view.dart';
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
/// Main widget for virtual reading that replaces ScrollablePositionedList
class ImageViewWebtoon extends StatelessWidget {
final List<UChapDataPreload> pages;
final ItemScrollController itemScrollController;
final ScrollOffsetController scrollOffsetController;
final ItemPositionsListener itemPositionsListener;
final Axis scrollDirection;
final double minCacheExtent;
final int initialScrollIndex;
final ScrollPhysics physics;
final Function(UChapDataPreload data) onLongPressData;
final Function(bool) onFailedToLoadImage;
final BackgroundColor backgroundColor;
final bool isDoublePageMode;
final bool isHorizontalContinuous;
final ReaderMode readerMode;
final PhotoViewController photoViewController;
final PhotoViewScaleStateController photoViewScaleStateController;
final Alignment scalePosition;
final Function(ScaleEndDetails) onScaleEnd;
final Function(Offset) onDoubleTapDown;
final VoidCallback onDoubleTap;
final int webtoonSidePadding;
final bool showPageGaps;
const ImageViewWebtoon({
super.key,
required this.pages,
required this.itemScrollController,
required this.scrollOffsetController,
required this.itemPositionsListener,
required this.scrollDirection,
required this.minCacheExtent,
required this.initialScrollIndex,
required this.physics,
required this.onLongPressData,
required this.onFailedToLoadImage,
required this.backgroundColor,
required this.isDoublePageMode,
required this.isHorizontalContinuous,
required this.readerMode,
required this.photoViewController,
required this.photoViewScaleStateController,
required this.scalePosition,
required this.onScaleEnd,
required this.onDoubleTapDown,
required this.onDoubleTap,
this.webtoonSidePadding = 0,
this.showPageGaps = true,
});
@override
Widget build(BuildContext context) {
return PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) => PhotoViewGalleryPageOptions.customChild(
controller: photoViewController,
scaleStateController: photoViewScaleStateController,
basePosition: scalePosition,
onScaleEnd: (context, details, controllerValue) => onScaleEnd(details),
child: ScrollablePositionedList.separated(
scrollDirection: scrollDirection,
minCacheExtent: minCacheExtent,
initialScrollIndex: initialScrollIndex,
itemCount: pages.length,
physics: physics,
itemScrollController: itemScrollController,
scrollOffsetController: scrollOffsetController,
itemPositionsListener: itemPositionsListener,
itemBuilder: (context, index) => _buildItem(context, index),
separatorBuilder: _buildSeparator,
),
),
);
}
Widget _buildItem(BuildContext context, int index) {
if (isDoublePageMode && !isHorizontalContinuous) {
return _buildDoublePageItem(context, index);
} else {
return _buildSinglePageItem(context, index);
}
}
Widget _buildSinglePageItem(BuildContext context, int index) {
final currentPage = pages[index];
final double sidePad = webtoonSidePadding > 0
? MediaQuery.of(context).size.width * webtoonSidePadding / 100
: 0;
if (currentPage.isTransitionPage) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: TransitionViewVertical(data: currentPage),
);
}
return Padding(
padding: isHorizontalContinuous
? EdgeInsets.zero
: EdgeInsets.symmetric(horizontal: sidePad),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: ImageViewVertical(
data: currentPage,
failedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
isHorizontal: isHorizontalContinuous,
),
),
);
}
Widget _buildDoublePageItem(BuildContext context, int index) {
final pageLength = pages.length;
if (index >= pageLength) {
return const SizedBox.shrink();
}
final int index1 = index * 2 - 1;
final int index2 = index1 + 1;
final List<UChapDataPreload?> datas = index == 0
? [pages[0], null]
: [
index1 < pageLength ? pages[index1] : null,
index2 < pageLength ? pages[index2] : null,
];
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: DoublePageView.vertical(
pages: datas,
backgroundColor: backgroundColor,
onFailedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
),
);
}
Widget _buildSeparator(BuildContext context, int index) {
if (!showPageGaps || readerMode == ReaderMode.webtoon) {
return const SizedBox.shrink();
}
if (isHorizontalContinuous) {
return VerticalDivider(
color: getBackgroundColor(backgroundColor),
width: 6,
);
} else {
return Divider(color: getBackgroundColor(backgroundColor), height: 6);
}
}
}