feat: Implement virtual scrolling for manga reader with optimized page management

- Added VirtualMangaList widget for displaying manga pages in a virtual scrolling list.
- Introduced VirtualPageManager to handle page loading, caching, and memory optimization.
- Created VirtualReaderView to integrate virtual scrolling with PhotoView for enhanced reading experience.
- Implemented page loading states and memory cleanup mechanisms in VirtualPageManager.
- Added debug information overlay for monitoring virtual page manager statistics.
- Enhanced user experience with callbacks for chapter transitions and page changes.
This commit is contained in:
Moustapha Kodjo Amadou 2025-05-28 14:56:05 +01:00
parent c3ac07fa97
commit c24df38506
4 changed files with 1341 additions and 560 deletions

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,343 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/reader_view.dart' as reader;
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(reader.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<reader.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

@ -0,0 +1,285 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart' as reader;
/// Page loading states for virtual scrolling
enum PageLoadState { notLoaded, loading, loaded, error, cached }
/// Virtual page information for tracking state
class VirtualPageInfo {
final int index;
final reader.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 isLoading => loadState == PageLoadState.loading;
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<reader.UChapDataPreload> _originalPages;
final VirtualPageConfig config;
final Map<int, VirtualPageInfo> _pageInfoMap = {};
final Set<int> _preloadQueue = {};
int _currentVisibleIndex = 0;
Timer? _cleanupTimer;
VirtualPageManager({
required List<reader.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 current visible index
int get currentVisibleIndex => _currentVisibleIndex;
/// 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
reader.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;
}
/// Get priority for a page (higher = more important)
int getPagePriority(int index) {
final distance = (index - _currentVisibleIndex).abs();
if (distance == 0) return 1000; // Current page has highest priority
return max(0, 100 - distance * 10);
}
/// 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);
}
}
}
// Process preload queue
_processPreloadQueue();
}
/// Process the preload queue
void _processPreloadQueue() {
final sortedQueue = _preloadQueue.toList()
..sort((a, b) => getPagePriority(b).compareTo(getPagePriority(a)));
for (final index in sortedQueue.take(3)) {
// Limit concurrent loading
_loadPage(index);
}
}
/// Load a specific page
Future<void> _loadPage(int index) async {
final pageInfo = _pageInfoMap[index];
if (pageInfo == null || pageInfo.isLoading) return;
pageInfo.loadState = PageLoadState.loading;
notifyListeners();
try {
// For now, we just mark as loaded since the actual image loading
// is handled by the ImageView widgets
await Future.delayed(const Duration(milliseconds: 10));
pageInfo.loadState = PageLoadState.loaded;
pageInfo.markAccessed();
} catch (error) {
pageInfo.loadState = PageLoadState.error;
pageInfo.error = error;
}
notifyListeners();
}
/// 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();
}
}
/// Force load a page immediately
Future<void> forceLoadPage(int index) async {
await _loadPage(index);
}
/// 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,
};
}
/// Preload a range of pages
Future<void> preloadRange(int startIndex, int endIndex) async {
for (int i = startIndex; i <= endIndex && i < _originalPages.length; i++) {
if (i >= 0) {
await _loadPage(i);
}
}
}
/// Clear all cached pages
void clearCache() {
for (final pageInfo in _pageInfoMap.values) {
if (pageInfo.loadState != PageLoadState.loading) {
pageInfo.loadState = PageLoadState.notLoaded;
pageInfo.error = null;
}
}
notifyListeners();
}
}

View file

@ -0,0 +1,225 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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';
import 'package:mangayomi/modules/manga/reader/reader_view.dart' as reader;
/// Provides virtual page manager instances
final virtualPageManagerProvider =
Provider.family<VirtualPageManager, List<reader.UChapDataPreload>>((
ref,
pages,
) {
return VirtualPageManager(pages: pages);
});
/// Main widget for virtual reading that replaces ScrollablePositionedList
class VirtualReaderView extends ConsumerStatefulWidget {
final List<reader.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(reader.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),
],
);
}
}
/// Mixin to add virtual page manager capabilities to existing widgets
mixin VirtualPageManagerMixin<T extends ConsumerStatefulWidget>
on ConsumerState<T> {
VirtualPageManager? _virtualPageManager;
VirtualPageManager get virtualPageManager {
_virtualPageManager ??= VirtualPageManager(pages: getPages());
return _virtualPageManager!;
}
/// Override this method to provide the pages list
List<reader.UChapDataPreload> getPages();
/// Call this when pages change
void updateVirtualPages(List<reader.UChapDataPreload> newPages) {
_virtualPageManager?.dispose();
_virtualPageManager = VirtualPageManager(pages: newPages);
}
/// Call this when the visible page changes
void updateVisiblePage(int index) {
virtualPageManager.updateVisibleIndex(index);
}
@override
void dispose() {
_virtualPageManager?.dispose();
super.dispose();
}
}
/// Configuration provider for virtual page manager
final virtualPageConfigProvider = Provider<VirtualPageConfig>((ref) {
// Get user preferences for virtual scrolling configuration
final preloadAmount = ref.watch(readerPagePreloadAmountStateProvider);
return VirtualPageConfig(
preloadDistance: preloadAmount,
maxCachedPages: preloadAmount * 3,
cacheTimeout: const Duration(minutes: 5),
enableMemoryOptimization: true,
);
});
/// Provider for page preload amount (renamed to avoid conflicts)
final readerPagePreloadAmountStateProvider = StateProvider<int>((ref) => 3);
/// Extension to convert ReaderMode to virtual scrolling parameters
extension ReaderModeExtension on ReaderMode {
bool get isContinuous {
return this == ReaderMode.verticalContinuous ||
this == ReaderMode.webtoon ||
this == ReaderMode.horizontalContinuous;
}
Axis get scrollDirection {
return this == ReaderMode.horizontalContinuous
? Axis.horizontal
: Axis.vertical;
}
bool get isHorizontalContinuous {
return this == ReaderMode.horizontalContinuous;
}
}