mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +00:00
Refactor manga reader components: replace VirtualReaderView with WebtoonView and remove unused virtual scrolling files
This commit is contained in:
parent
8eea4eaa4d
commit
f483dfab2b
5 changed files with 201 additions and 764 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
160
lib/modules/manga/reader/webtoon_view.dart
Normal file
160
lib/modules/manga/reader/webtoon_view.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue