Refactor manga reader components: replace VirtualReaderView with WebtoonView and remove unused virtual scrolling files

This commit is contained in:
Moustapha Kodjo Amadou 2025-11-06 11:48:12 +01:00
parent 8eea4eaa4d
commit f483dfab2b
5 changed files with 201 additions and 764 deletions

View file

@ -40,10 +40,9 @@ import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicat
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_paged.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/modules/manga/reader/providers/manga_reader_provider.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_reader_view.dart';
import 'package:mangayomi/modules/manga/reader/webtoon_view.dart';
import 'package:mangayomi/modules/widgets/progress_center.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:share_plus/share_plus.dart';
import 'package:super_sliver_list/super_sliver_list.dart';
@ -243,7 +242,7 @@ class _MangaChapterPageGalleryState
WidgetsBinding.instance.addObserver(this);
}
final double _horizontalScaleValue = 1.0;
// final double _horizontalScaleValue = 1.0;
bool _isNextChapterPreloading = false;
late int pagePreloadAmount = ref.read(pagePreloadAmountStateProvider);
@ -565,63 +564,45 @@ class _MangaChapterPageGalleryState
return Stack(
children: [
_isVerticalOrHorizontalContinous()
? PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) =>
PhotoViewGalleryPageOptions.customChild(
controller: _photoViewController,
scaleStateController:
_photoViewScaleStateController,
basePosition: _scalePosition,
onScaleEnd: _onScaleEnd,
child: VirtualReaderView(
pages: _uChapDataPreload,
itemScrollController: _itemScrollController,
scrollOffsetController:
_pageOffsetController,
itemPositionsListener:
_itemPositionsListener,
scrollDirection: isHorizontalContinuaous
? Axis.horizontal
: Axis.vertical,
minCacheExtent:
pagePreloadAmount * context.height(1),
initialScrollIndex: _readerController
.getPageIndex(),
physics: const ClampingScrollPhysics(),
onLongPressData: (data) =>
_onLongPressImageDialog(data, context),
onFailedToLoadImage: (value) {
// Handle failed image loading
if (_failedToLoadImage.value != value &&
context.mounted) {
_failedToLoadImage.value = value;
}
},
backgroundColor: backgroundColor,
isDoublePageMode:
_pageMode == PageMode.doublePage &&
!isHorizontalContinuaous,
isHorizontalContinuous:
isHorizontalContinuaous,
readerMode: ref.watch(_currentReaderMode)!,
photoViewController: _photoViewController,
photoViewScaleStateController:
_photoViewScaleStateController,
scalePosition: _scalePosition,
onScaleEnd: (details) => _onScaleEnd(
context,
details,
_photoViewController.value,
),
onDoubleTapDown: (offset) =>
_toggleScale(offset),
onDoubleTap: () {},
// Chapter transition callbacks
onChapterChanged: (newChapter) {},
onReachedLastPage: (lastPageIndex) {},
),
),
? WebtoonView(
pages: _uChapDataPreload,
itemScrollController: _itemScrollController,
scrollOffsetController: _pageOffsetController,
itemPositionsListener: _itemPositionsListener,
scrollDirection: isHorizontalContinuaous
? Axis.horizontal
: Axis.vertical,
minCacheExtent:
pagePreloadAmount * context.height(1),
initialScrollIndex: _readerController
.getPageIndex(),
physics: const ClampingScrollPhysics(),
onLongPressData: (data) =>
_onLongPressImageDialog(data, context),
onFailedToLoadImage: (value) {
// Handle failed image loading
if (_failedToLoadImage.value != value &&
context.mounted) {
_failedToLoadImage.value = value;
}
},
backgroundColor: backgroundColor,
isDoublePageMode:
_pageMode == PageMode.doublePage &&
!isHorizontalContinuaous,
isHorizontalContinuous: isHorizontalContinuaous,
readerMode: ref.watch(_currentReaderMode)!,
photoViewController: _photoViewController,
photoViewScaleStateController:
_photoViewScaleStateController,
scalePosition: _scalePosition,
onScaleEnd: (details) => _onScaleEnd(
context,
details,
_photoViewController.value,
),
onDoubleTapDown: (offset) => _toggleScale(offset),
onDoubleTap: () {},
)
: Material(
color: getBackgroundColor(backgroundColor),

View file

@ -1,343 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart';
import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart';
import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
/// Widget for displaying manga pages in a virtual scrolling list
class VirtualMangaList extends ConsumerStatefulWidget {
final VirtualPageManager pageManager;
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 Function(Offset) onDoubleTapDown;
final VoidCallback onDoubleTap;
final Function(Chapter chapter)? onChapterChanged;
final Function(int lastPageIndex)? onReachedLastPage;
final Function(int index)? onPageChanged;
const VirtualMangaList({
super.key,
required this.pageManager,
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.onDoubleTapDown,
required this.onDoubleTap,
this.onChapterChanged,
this.onReachedLastPage,
this.onPageChanged,
});
@override
ConsumerState<VirtualMangaList> createState() => _VirtualMangaListState();
}
class _VirtualMangaListState extends ConsumerState<VirtualMangaList> {
Chapter? _currentChapter;
int? _currentIndex;
@override
void initState() {
super.initState();
// Listen to item positions to update virtual page manager
widget.itemPositionsListener.itemPositions.addListener(_onPositionChanged);
// Initialize current chapter
if (widget.pageManager.pageCount > 0) {
final firstPage = widget.pageManager.getOriginalPage(
widget.initialScrollIndex,
);
_currentChapter = firstPage?.chapter;
}
}
@override
void dispose() {
widget.itemPositionsListener.itemPositions.removeListener(
_onPositionChanged,
);
super.dispose();
}
void _onPositionChanged() {
final positions = widget.itemPositionsListener.itemPositions.value;
if (positions.isNotEmpty) {
// Get the first visible item
final firstVisibleIndex = positions.first.index;
final lastVisibleIndex = positions.last.index;
// Update virtual page manager
widget.pageManager.updateVisibleIndex(firstVisibleIndex);
// Calculate actual page lengths considering page mode
int pagesLength =
widget.isDoublePageMode && !widget.isHorizontalContinuous
? (widget.pageManager.pageCount / 2).ceil() + 1
: widget.pageManager.pageCount;
// Check if index is valid
if (firstVisibleIndex >= 0 && firstVisibleIndex < pagesLength) {
final currentPage = widget.pageManager.getOriginalPage(
firstVisibleIndex,
);
if (currentPage != null) {
// Check for chapter change
if (_currentChapter?.id != currentPage.chapter?.id &&
currentPage.chapter != null) {
_currentChapter = currentPage.chapter;
widget.onChapterChanged?.call(currentPage.chapter!);
}
// Update current index
if (_currentIndex != firstVisibleIndex) {
_currentIndex = firstVisibleIndex;
widget.onPageChanged?.call(firstVisibleIndex);
}
}
// Check if reached last page to trigger next chapter preload
if (lastVisibleIndex >= pagesLength - 1) {
widget.onReachedLastPage?.call(lastVisibleIndex);
}
}
}
}
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: widget.pageManager,
builder: (context, child) {
final itemCount =
widget.isDoublePageMode && !widget.isHorizontalContinuous
? (widget.pageManager.pageCount / 2).ceil() + 1
: widget.pageManager.pageCount;
return ScrollablePositionedList.separated(
scrollDirection: widget.scrollDirection,
minCacheExtent: widget.minCacheExtent,
initialScrollIndex: widget.initialScrollIndex,
itemCount: itemCount,
physics: widget.physics,
itemScrollController: widget.itemScrollController,
scrollOffsetController: widget.scrollOffsetController,
itemPositionsListener: widget.itemPositionsListener,
itemBuilder: (context, index) => _buildItem(context, index),
separatorBuilder: _buildSeparator,
);
},
);
}
Widget _buildItem(BuildContext context, int index) {
if (widget.isDoublePageMode && !widget.isHorizontalContinuous) {
return _buildDoublePageItem(context, index);
} else {
return _buildSinglePageItem(context, index);
}
}
Widget _buildSinglePageItem(BuildContext context, int index) {
final originalPage = widget.pageManager.getOriginalPage(index);
if (originalPage == null) {
return const SizedBox.shrink();
}
// Check if page should be loaded
final pageInfo = widget.pageManager.getPageInfo(index);
final shouldLoad = widget.pageManager.shouldPageBeLoaded(index);
if (!shouldLoad &&
(pageInfo?.loadState == PageLoadState.notLoaded || pageInfo == null)) {
// Return placeholder for unloaded pages
return _buildPlaceholder(context);
}
if (originalPage.isTransitionPage) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) =>
widget.onDoubleTapDown(details.globalPosition),
onDoubleTap: widget.onDoubleTap,
child: TransitionViewVertical(data: originalPage),
);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) =>
widget.onDoubleTapDown(details.globalPosition),
onDoubleTap: widget.onDoubleTap,
child: ImageViewVertical(
data: originalPage,
failedToLoadImage: widget.onFailedToLoadImage,
onLongPressData: widget.onLongPressData,
isHorizontal: widget.isHorizontalContinuous,
),
);
}
Widget _buildDoublePageItem(BuildContext context, int index) {
if (index >= widget.pageManager.pageCount) {
return const SizedBox.shrink();
}
final int index1 = index * 2 - 1;
final int index2 = index1 + 1;
final List<UChapDataPreload?> datas = index == 0
? [widget.pageManager.getOriginalPage(0), null]
: [
index1 < widget.pageManager.pageCount
? widget.pageManager.getOriginalPage(index1)
: null,
index2 < widget.pageManager.pageCount
? widget.pageManager.getOriginalPage(index2)
: null,
];
// Check if pages should be loaded
final shouldLoad1 = index1 >= 0
? widget.pageManager.shouldPageBeLoaded(index1)
: false;
final shouldLoad2 = index2 < widget.pageManager.pageCount
? widget.pageManager.shouldPageBeLoaded(index2)
: false;
if (!shouldLoad1 && !shouldLoad2) {
return _buildPlaceholder(context);
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) =>
widget.onDoubleTapDown(details.globalPosition),
onDoubleTap: widget.onDoubleTap,
child: DoubleColummVerticalView(
datas: datas,
backgroundColor: widget.backgroundColor,
isFailedToLoadImage: widget.onFailedToLoadImage,
onLongPressData: widget.onLongPressData,
),
);
}
Widget _buildPlaceholder(BuildContext context) {
return Container(
height: context.height(0.8),
color: getBackgroundColor(widget.backgroundColor),
child: const Center(child: CircularProgressIndicator()),
);
}
Widget _buildSeparator(BuildContext context, int index) {
if (widget.readerMode == ReaderMode.webtoon) {
return const SizedBox.shrink();
}
if (widget.isHorizontalContinuous) {
return VerticalDivider(
color: getBackgroundColor(widget.backgroundColor),
width: 6,
);
} else {
return Divider(
color: getBackgroundColor(widget.backgroundColor),
height: 6,
);
}
}
}
/// Debug widget to show virtual page manager statistics
class VirtualPageManagerDebugInfo extends ConsumerWidget {
final VirtualPageManager pageManager;
const VirtualPageManagerDebugInfo({super.key, required this.pageManager});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListenableBuilder(
listenable: pageManager,
builder: (context, child) {
final stats = pageManager.getMemoryStats();
return Positioned(
top: 100,
right: 10,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Virtual Page Manager',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'Current: ${stats['currentIndex']}/${stats['totalPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Loaded: ${stats['loadedPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Cached: ${stats['cachedPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Errors: ${stats['errorPages']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
Text(
'Queue: ${stats['preloadQueueSize']}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
);
},
);
}
}

View file

@ -1,211 +0,0 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
/// Page loading states for virtual scrolling
enum PageLoadState { notLoaded, loaded, error, cached }
/// Virtual page information for tracking state
class VirtualPageInfo {
final int index;
final UChapDataPreload originalData;
PageLoadState loadState;
DateTime? lastAccessTime;
Object? error;
VirtualPageInfo({
required this.index,
required this.originalData,
this.loadState = PageLoadState.notLoaded,
this.lastAccessTime,
this.error,
});
bool get isVisible =>
loadState == PageLoadState.loaded || loadState == PageLoadState.cached;
bool get needsLoading => loadState == PageLoadState.notLoaded;
bool get hasError => loadState == PageLoadState.error;
void markAccessed() {
lastAccessTime = DateTime.now();
}
Duration get timeSinceAccess {
if (lastAccessTime == null) return Duration.zero;
return DateTime.now().difference(lastAccessTime!);
}
}
/// Configuration for virtual page manager
class VirtualPageConfig {
final int preloadDistance;
final int maxCachedPages;
final Duration cacheTimeout;
final bool enableMemoryOptimization;
const VirtualPageConfig({
this.preloadDistance = 3,
this.maxCachedPages = 10,
this.cacheTimeout = const Duration(minutes: 5),
this.enableMemoryOptimization = true,
});
}
/// Manages virtual page loading and memory optimization
class VirtualPageManager extends ChangeNotifier {
final List<UChapDataPreload> _originalPages;
final VirtualPageConfig config;
final Map<int, VirtualPageInfo> _pageInfoMap = {};
final Set<int> _preloadQueue = {};
int _currentVisibleIndex = 0;
Timer? _cleanupTimer;
VirtualPageManager({
required List<UChapDataPreload> pages,
this.config = const VirtualPageConfig(),
}) : _originalPages = List.from(pages) {
_initializePages();
_startCleanupTimer();
}
void _initializePages() {
for (int i = 0; i < _originalPages.length; i++) {
_pageInfoMap[i] = VirtualPageInfo(
index: i,
originalData: _originalPages[i],
);
}
}
void _startCleanupTimer() {
_cleanupTimer?.cancel();
_cleanupTimer = Timer.periodic(
const Duration(seconds: 30),
(_) => _performMemoryCleanup(),
);
}
@override
void dispose() {
_cleanupTimer?.cancel();
super.dispose();
}
/// Get page count
int get pageCount => _originalPages.length;
/// Get page info for a specific index
VirtualPageInfo? getPageInfo(int index) {
if (index < 0 || index >= _originalPages.length) return null;
return _pageInfoMap[index];
}
/// Get original page data
UChapDataPreload? getOriginalPage(int index) {
if (index < 0 || index >= _originalPages.length) return null;
return _originalPages[index];
}
/// Update visible page index and trigger preloading
void updateVisibleIndex(int index) {
if (index == _currentVisibleIndex) return;
_currentVisibleIndex = index.clamp(0, _originalPages.length - 1);
_pageInfoMap[_currentVisibleIndex]?.markAccessed();
_schedulePreloading();
notifyListeners();
}
/// Check if a page should be visible/loaded
bool shouldPageBeLoaded(int index) {
final distance = (index - _currentVisibleIndex).abs();
return distance <= config.preloadDistance;
}
/// Schedule preloading for nearby pages
void _schedulePreloading() {
_preloadQueue.clear();
// Add pages within preload distance
for (int i = 0; i < _originalPages.length; i++) {
if (shouldPageBeLoaded(i)) {
final pageInfo = _pageInfoMap[i]!;
if (pageInfo.needsLoading) {
_preloadQueue.add(i);
}
}
}
}
/// Perform memory cleanup
void _performMemoryCleanup() {
if (!config.enableMemoryOptimization) return;
final pageEntries = _pageInfoMap.entries.toList();
// Sort by last access time and distance from current page
pageEntries.sort((a, b) {
final aDistance = (a.key - _currentVisibleIndex).abs();
final bDistance = (b.key - _currentVisibleIndex).abs();
final aTime =
a.value.lastAccessTime ?? DateTime.fromMillisecondsSinceEpoch(0);
final bTime =
b.value.lastAccessTime ?? DateTime.fromMillisecondsSinceEpoch(0);
// First sort by distance, then by access time
final distanceComparison = aDistance.compareTo(bDistance);
return distanceComparison != 0
? distanceComparison
: aTime.compareTo(bTime);
});
int cachedCount = pageEntries.where((e) => e.value.isVisible).length;
// Remove old cached pages if we exceed the limit
for (final entry in pageEntries) {
if (cachedCount <= config.maxCachedPages) break;
final pageInfo = entry.value;
final distance = (entry.key - _currentVisibleIndex).abs();
// Don't unload pages within preload distance
if (distance <= config.preloadDistance) continue;
// Don't unload recently accessed pages
if (pageInfo.timeSinceAccess < config.cacheTimeout) continue;
if (pageInfo.isVisible) {
pageInfo.loadState = PageLoadState.notLoaded;
pageInfo.error = null;
cachedCount--;
}
}
if (cachedCount != pageEntries.where((e) => e.value.isVisible).length) {
notifyListeners();
}
}
/// Get memory usage statistics
Map<String, dynamic> getMemoryStats() {
final loadedCount = _pageInfoMap.values
.where((p) => p.loadState == PageLoadState.loaded)
.length;
final cachedCount = _pageInfoMap.values
.where((p) => p.loadState == PageLoadState.cached)
.length;
final errorCount = _pageInfoMap.values.where((p) => p.hasError).length;
return {
'totalPages': _originalPages.length,
'loadedPages': loadedCount,
'cachedPages': cachedCount,
'errorPages': errorCount,
'currentIndex': _currentVisibleIndex,
'preloadQueueSize': _preloadQueue.length,
};
}
}

View file

@ -1,150 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.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';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart';
import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart';
/// Main widget for virtual reading that replaces ScrollablePositionedList
class VirtualReaderView extends ConsumerStatefulWidget {
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 bool showDebugInfo;
// Callbacks pour gérer les transitions entre chapitres
final Function(Chapter chapter)? onChapterChanged;
final Function(int lastPageIndex)? onReachedLastPage;
const VirtualReaderView({
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.showDebugInfo = false,
this.onChapterChanged,
this.onReachedLastPage,
});
@override
ConsumerState<VirtualReaderView> createState() => _VirtualReaderViewState();
}
class _VirtualReaderViewState extends ConsumerState<VirtualReaderView> {
late VirtualPageManager _pageManager;
@override
void initState() {
super.initState();
_pageManager = VirtualPageManager(pages: widget.pages);
// Set initial visible index
_pageManager.updateVisibleIndex(widget.initialScrollIndex);
}
@override
void didUpdateWidget(VirtualReaderView oldWidget) {
super.didUpdateWidget(oldWidget);
// Update page manager if pages changed
if (widget.pages != oldWidget.pages) {
_pageManager.dispose();
_pageManager = VirtualPageManager(pages: widget.pages);
_pageManager.updateVisibleIndex(widget.initialScrollIndex);
}
}
@override
void dispose() {
_pageManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_pageManager.pageCount < widget.pages.length) {
_pageManager = VirtualPageManager(pages: widget.pages);
}
return Stack(
children: [
PhotoViewGallery.builder(
itemCount: 1,
builder: (_, _) => PhotoViewGalleryPageOptions.customChild(
controller: widget.photoViewController,
scaleStateController: widget.photoViewScaleStateController,
basePosition: widget.scalePosition,
onScaleEnd: (context, details, controllerValue) =>
widget.onScaleEnd(details),
child: VirtualMangaList(
pageManager: _pageManager,
itemScrollController: widget.itemScrollController,
scrollOffsetController: widget.scrollOffsetController,
itemPositionsListener: widget.itemPositionsListener,
scrollDirection: widget.scrollDirection,
minCacheExtent: widget.minCacheExtent,
initialScrollIndex: widget.initialScrollIndex,
physics: widget.physics,
onLongPressData: widget.onLongPressData,
onFailedToLoadImage: widget.onFailedToLoadImage,
backgroundColor: widget.backgroundColor,
isDoublePageMode: widget.isDoublePageMode,
isHorizontalContinuous: widget.isHorizontalContinuous,
readerMode: widget.readerMode,
onDoubleTapDown: widget.onDoubleTapDown,
onDoubleTap: widget.onDoubleTap,
// Passer les callbacks pour les transitions entre chapitres
onChapterChanged: widget.onChapterChanged,
onReachedLastPage: widget.onReachedLastPage,
onPageChanged: (index) {
// Ici on peut ajouter une logique supplémentaire si nécessaire
// Par exemple, précaching d'images
_pageManager.updateVisibleIndex(index);
},
),
),
),
// Debug info overlay
if (widget.showDebugInfo)
VirtualPageManagerDebugInfo(pageManager: _pageManager),
],
);
}
}

View file

@ -0,0 +1,160 @@
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.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 WebtoonView 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;
const WebtoonView({
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,
});
@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];
if (currentPage.isTransitionPage) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition),
onDoubleTap: onDoubleTap,
child: TransitionViewVertical(data: currentPage),
);
}
return 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: DoubleColummVerticalView(
datas: datas,
backgroundColor: backgroundColor,
isFailedToLoadImage: onFailedToLoadImage,
onLongPressData: onLongPressData,
),
);
}
Widget _buildSeparator(BuildContext context, int index) {
if (readerMode == ReaderMode.webtoon) {
return const SizedBox.shrink();
}
if (isHorizontalContinuous) {
return VerticalDivider(
color: getBackgroundColor(backgroundColor),
width: 6,
);
} else {
return Divider(color: getBackgroundColor(backgroundColor), height: 6);
}
}
}