From 9e795106f83578bb15ebe289ba1a1b0c45265a6a Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:52:57 +0200 Subject: [PATCH 01/23] Fix page jumps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ChapterPreloadManager.preloadPrevChapter` does `_currentIndex += prependCount` (internal manager index). Then `_handlePrevChapterPrepended` in `reader_view.dart` does the exact same thing again to the UI’s `_currentIndex` before calling `jumpTo`. The UI state already handles the adjustment + `jumpTo` correctly. The manager’s internal `_currentIndex` is not needed for continuous mode (the `ItemPositionsListener` overrides it anyway). --- lib/modules/manga/reader/managers/chapter_preload_manager.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/modules/manga/reader/managers/chapter_preload_manager.dart b/lib/modules/manga/reader/managers/chapter_preload_manager.dart index 2be7343d..a9042127 100644 --- a/lib/modules/manga/reader/managers/chapter_preload_manager.dart +++ b/lib/modules/manga/reader/managers/chapter_preload_manager.dart @@ -263,9 +263,6 @@ class ChapterPreloadManager { // Prepend to pages list _pages.insertAll(0, prependList); - // Update current index to account for prepended pages - _currentIndex += prependCount; - // Track the new chapter if (chapterId != null) { _loadedChapterIds.add(chapterId); From b972915391f3d0af9c8367f3263b960e1121b8b3 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:13:27 +0200 Subject: [PATCH 02/23] Spelling error --- lib/modules/manga/reader/reader_view.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index a57de7e7..984882e5 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -346,7 +346,7 @@ class _MangaChapterPageGalleryState final fullScreenReader = ref.watch(fullScreenReaderStateProvider); final cropBorders = ref.watch(cropBordersStateProvider); final readerMode = ref.watch(_currentReaderMode); - final bool isHorizontalContinuaous = + final bool isHorizontalContinuous = readerMode == ReaderMode.horizontalContinuous || readerMode == ReaderMode.horizontalContinuousRTL; if (cropBorders) { @@ -419,7 +419,7 @@ class _MangaChapterPageGalleryState itemScrollController: _itemScrollController, scrollOffsetController: _pageOffsetController, itemPositionsListener: _itemPositionsListener, - scrollDirection: isHorizontalContinuaous + scrollDirection: isHorizontalContinuous ? Axis.horizontal : Axis.vertical, minCacheExtent: @@ -443,8 +443,8 @@ class _MangaChapterPageGalleryState backgroundColor: backgroundColor, isDoublePageMode: _pageMode == PageMode.doublePage && - !isHorizontalContinuaous, - isHorizontalContinuous: isHorizontalContinuaous, + !isHorizontalContinuous, + isHorizontalContinuous: isHorizontalContinuous, readerMode: ref.watch(_currentReaderMode)!, photoViewController: _photoViewController, photoViewScaleStateController: @@ -468,7 +468,7 @@ class _MangaChapterPageGalleryState shadowColor: getBackgroundColor(backgroundColor), child: (_pageMode == PageMode.doublePage && - !isHorizontalContinuaous) + !isHorizontalContinuous) ? ExtendedImageGesturePageView.builder( controller: _extendedController, scrollDirection: _scrollDirection, From c0443edff4ab500411064be24a2c4ae7e48fdbdc Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Fri, 10 Apr 2026 21:49:39 +0200 Subject: [PATCH 03/23] Fix list rebuild jank by adding stable page keys Introduce a unique `ValueKey` for each page (chapter ID + page index) and wrap items in `KeyedSubtree`. This ensures Flutter can correctly preserve widget identity when the preload manager inserts or prepends pages. Previously, every `setState` triggered by page preloading caused the entire list to rebuild, leading to visible lag. With stable keys, only newly added pages rebuild while existing ones retain their state. This significantly reduces jank, improves scroll smoothness, and makes chapter transitions nearly seamless. --- lib/modules/manga/reader/image_view_webtoon.dart | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/modules/manga/reader/image_view_webtoon.dart b/lib/modules/manga/reader/image_view_webtoon.dart index 094e583a..14f3df19 100644 --- a/lib/modules/manga/reader/image_view_webtoon.dart +++ b/lib/modules/manga/reader/image_view_webtoon.dart @@ -89,11 +89,17 @@ class ImageViewWebtoon extends StatelessWidget { } Widget _buildItem(BuildContext context, int index) { - if (isDoublePageMode && !isHorizontalContinuous) { - return _buildDoublePageItem(context, index); - } else { - return _buildSinglePageItem(context, index); - } + final currentPage = pages[index]; + final uniqueKey = ValueKey( + '${currentPage.chapter?.id ?? "trans"}-${currentPage.index ?? index}', + ); + + return KeyedSubtree( + key: uniqueKey, + child: (isDoublePageMode && !isHorizontalContinuous) + ? _buildDoublePageItem(context, index) + : _buildSinglePageItem(context, index), + ); } Widget _buildSinglePageItem(BuildContext context, int index) { From 5b888fbe43a572e82b683e0fec7315629d37007c Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Fri, 10 Apr 2026 23:03:18 +0200 Subject: [PATCH 04/23] TODO comment, so we don't forget --- lib/modules/manga/reader/reader_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 984882e5..e733f3ad 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -434,7 +434,7 @@ class _MangaChapterPageGalleryState chapterName: widget.chapter.name!, ), onFailedToLoadImage: (value) { - // // Handle failed image loading + // TODO: Handle failed image loading // if (_failedToLoadImage.value != value && // context.mounted) { // _failedToLoadImage.value = value; From 1735c80014b793797ef5dfef47cedbf029beebe1 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 11 Apr 2026 17:35:34 +0200 Subject: [PATCH 05/23] Fix page jumps fix for the "jump back" bug that occurred when scrolling up in vertical continuous and webtoon reader modes. - `ChapterPreloadManager` was maintaining its own `_currentIndex`. - When prepending previous chapter pages, the index was being incremented twice (once in the manager + once in `_handlePrevChapterPrepended`). - This caused `itemScrollController.jumpTo()` to overshoot, resulting in a visible jump forward (perceived as "jump back" while trying to scroll up). - Removed all index management (`_currentIndex`, getter, setter, startIndex) from `ChapterPreloadManager` and `ReaderMemoryManagement`. - `ChapterPreloadManager` is now a pure data container (only manages `_pages`). - `_handlePrevChapterPrepended` now captures the **current visible top index** *before* prepending and adjusts the scroll position only once. - `_readProgressListener` is now the single source of truth for `_currentIndex`. - Removed stale `initialScrollIndex` logic from preload initialization. --- .../managers/chapter_preload_manager.dart | 16 +-- .../mixins/reader_memory_management.dart | 12 +- lib/modules/manga/reader/reader_view.dart | 105 ++++++++++-------- 3 files changed, 59 insertions(+), 74 deletions(-) diff --git a/lib/modules/manga/reader/managers/chapter_preload_manager.dart b/lib/modules/manga/reader/managers/chapter_preload_manager.dart index a9042127..ca7a4d35 100644 --- a/lib/modules/manga/reader/managers/chapter_preload_manager.dart +++ b/lib/modules/manga/reader/managers/chapter_preload_manager.dart @@ -20,9 +20,6 @@ class ChapterPreloadManager { /// Queue of chapter IDs in order of loading (for LRU eviction) final Queue _chapterLoadOrder = Queue(); - /// Current reading index - int _currentIndex = 0; - /// Separate flags to allow concurrent prev/next preloading bool _isPreloadingNext = false; bool _isPreloadingPrev = false; @@ -36,9 +33,6 @@ class ChapterPreloadManager { /// Gets the current number of pages int get pageCount => _pages.length; - /// Gets the current index - int get currentIndex => _currentIndex; - /// Gets the loaded chapter count int get loadedChapterCount => _loadedChapterIds.length; @@ -48,13 +42,6 @@ class ChapterPreloadManager { /// Whether a next chapter preload is in progress. bool get isPreloadingNext => _isPreloadingNext; - /// Sets the current reading index - set currentIndex(int value) { - if (value >= 0 && value < _pages.length) { - _currentIndex = value; - } - } - /// Returns `true` if pages from [chapter] are already in memory. bool isChapterLoaded(Chapter? chapter) { final id = _getChapterIdentifier(chapter); @@ -62,13 +49,12 @@ class ChapterPreloadManager { } /// Initializes the manager with the first chapter's pages. - void initialize(List initialPages, int startIndex) { + void initialize(List initialPages) { _pages.clear(); _loadedChapterIds.clear(); _chapterLoadOrder.clear(); _pages.addAll(initialPages); - _currentIndex = startIndex; // Track the initial chapter if (initialPages.isNotEmpty) { diff --git a/lib/modules/manga/reader/mixins/reader_memory_management.dart b/lib/modules/manga/reader/mixins/reader_memory_management.dart index fc332991..d4d3a140 100644 --- a/lib/modules/manga/reader/mixins/reader_memory_management.dart +++ b/lib/modules/manga/reader/mixins/reader_memory_management.dart @@ -20,23 +20,13 @@ mixin ReaderMemoryManagement { /// Gets the total page count. int get pageCount => _preloadManager.pageCount; - /// Gets the current page index. - int get currentPageIndex => _preloadManager.currentIndex; - - /// Sets the current page index. - set currentPageIndex(int value) { - _preloadManager.currentIndex = value; - } - /// Initializes the preload manager with initial chapter data. /// /// [chapterData] - The initial chapter pages to load. - /// [startIndex] - The initial page index (default: 0). /// [onPagesUpdated] - Callback when pages are added/removed. /// [onIndexAdjusted] - Callback when current index needs adjustment. void initializePreloadManager( GetChapterPagesModel chapterData, { - int startIndex = 0, VoidCallback? onPagesUpdated, }) { if (_isPreloadManagerInitialized) { @@ -48,7 +38,7 @@ mixin ReaderMemoryManagement { _preloadManager.onPagesUpdated = onPagesUpdated; - _preloadManager.initialize(chapterData.uChapDataPreload, startIndex); + _preloadManager.initialize(chapterData.uChapDataPreload); _isPreloadManagerInitialized = true; diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index e733f3ad..36fdcdf0 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -958,57 +958,56 @@ class _MangaChapterPageGalleryState void _readProgressListener() async { if (_isAdjustingScroll) return; final itemPositions = _itemPositionsListener.itemPositions.value; - if (itemPositions.isNotEmpty) { - _currentIndex = itemPositions.first.index; - int pagesLength = - (_pageMode == PageMode.doublePage && - !(ref.watch(_currentReaderMode) == - ReaderMode.horizontalContinuous || - ref.watch(_currentReaderMode) == - ReaderMode.horizontalContinuousRTL)) - ? (pages.length / 2).ceil() - : pages.length; - if (_currentIndex! >= 0 && _currentIndex! < pagesLength) { - if (_readerController.chapter.id != pages[_currentIndex!].chapter!.id) { - if (mounted) { - setState(() { - _readerController = ref.read( - readerControllerProvider( - chapter: pages[_currentIndex!].chapter!, - ).notifier, - ); + if (itemPositions.isEmpty) return; + _currentIndex = itemPositions.first.index; + int pagesLength = + (_pageMode == PageMode.doublePage && + !(ref.watch(_currentReaderMode) == + ReaderMode.horizontalContinuous || + ref.watch(_currentReaderMode) == + ReaderMode.horizontalContinuousRTL)) + ? (pages.length / 2).ceil() + : pages.length; + if (_currentIndex! >= 0 && _currentIndex! < pagesLength) { + if (_readerController.chapter.id != pages[_currentIndex!].chapter!.id) { + if (mounted) { + setState(() { + _readerController = ref.read( + readerControllerProvider( + chapter: pages[_currentIndex!].chapter!, + ).notifier, + ); - chapter = pages[_currentIndex!].chapter!; - final chapterUrlModel = pages[_currentIndex!].chapterUrlModel; + chapter = pages[_currentIndex!].chapter!; + final chapterUrlModel = pages[_currentIndex!].chapterUrlModel; - if (chapterUrlModel != null) { - _chapterUrlModel = chapterUrlModel; - } + if (chapterUrlModel != null) { + _chapterUrlModel = chapterUrlModel; + } - _isBookmarked = _readerController.getChapterBookmarked(); - }); - } + _isBookmarked = _readerController.getChapterBookmarked(); + }); } + } - // ── Next-chapter preloading: trigger when near the end ── - final distToEnd = pagesLength - 1 - itemPositions.last.index; - if (distToEnd <= pagePreloadAmount && !_isLastPageTransition) { - _triggerNextChapterPreload(); - } + // ── Next-chapter preloading: trigger when near the end ── + final distToEnd = pagesLength - 1 - itemPositions.last.index; + if (distToEnd <= pagePreloadAmount && !_isLastPageTransition) { + _triggerNextChapterPreload(); + } - // ── Previous-chapter preloading: trigger when near the start ── - if (itemPositions.first.index <= pagePreloadAmount) { - _triggerPrevChapterPreload(); - } + // ── Previous-chapter preloading: trigger when near the start ── + if (itemPositions.first.index <= pagePreloadAmount) { + _triggerPrevChapterPreload(); + } - final idx = pages[_currentIndex!].index; - if (idx != null) { - _readerController.setPageIndex( - _isDoublePageActive ? idx : _geCurrentIndex(idx), - false, - ); - ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex(idx); - } + final idx = pages[_currentIndex!].index; + if (idx != null) { + _readerController.setPageIndex( + _isDoublePageActive ? idx : _geCurrentIndex(idx), + false, + ); + ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex(idx); } } } @@ -1108,24 +1107,35 @@ class _MangaChapterPageGalleryState ) { try { if (chapterData.uChapDataPreload.isEmpty || !mounted) return; + + // Record the CURRENT visible top index BEFORE prepending + final currentVisibleItems = _itemPositionsListener.itemPositions.value; + final oldTopIndex = currentVisibleItems.isNotEmpty + ? currentVisibleItems.first.index + : _currentIndex ?? 0; + preloadPreviousChapter(chapterData, chap).then((prependCount) { if (prependCount > 0 && mounted) { _isAdjustingScroll = true; + + // New index = old visible index + how many items we just prepended + final newIndex = oldTopIndex + prependCount; + // In double page mode, _currentIndex stores the page view index, // so convert the prepended page count to page view units. if (_isDoublePageActive) { // Recompute the page view index from the new actual index. - final oldActual = _pageViewToActualIndex(_currentIndex!); + final oldActual = _pageViewToActualIndex(oldTopIndex); final newActual = oldActual + prependCount; _currentIndex = _actualToPageViewIndex(newActual); } else { - _currentIndex = _currentIndex! + prependCount; + _currentIndex = newIndex; } setState(() {}); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { if (_isContinuousMode()) { - _itemScrollController.jumpTo(index: _currentIndex!); + _itemScrollController.jumpTo(index: newIndex); } else if (_extendedController.hasClients) { _extendedController.jumpToPage(_currentIndex!); } @@ -1143,7 +1153,6 @@ class _MangaChapterPageGalleryState // Initialize the preload manager with bounded memory (from ReaderMemoryManagement mixin) initializePreloadManager( _chapterUrlModel, - startIndex: _currentIndex ?? 0, onPagesUpdated: () { if (mounted) setState(() {}); }, From 590ce38f29871faa45db8c50fb144ab1bbda79a4 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:59:33 +0200 Subject: [PATCH 06/23] save last index --- .../manga/reader/providers/reader_controller_provider.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index 6e67aced..2c4c35d2 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -353,9 +353,11 @@ class ReaderController extends _$ReaderController { return urls.length; } + int? _lastSavedIndex; void setPageIndex(int newIndex, bool save) { - if (chapter.isRead!) return; - if (incognitoMode) return; + if (chapter.isRead! || incognitoMode) return; + if (!save && newIndex == _lastSavedIndex) return; + _lastSavedIndex = newIndex; final isContinuousLike = getReaderMode() == ReaderMode.verticalContinuous || getReaderMode() == ReaderMode.webtoon; From 64a28d822df4145f11ff55c3fe57d37d1561c2e1 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:06:31 +0200 Subject: [PATCH 07/23] CropBorders performance+ call `_processCropBorders()` once, from _initCurrentIndex and after new pages arrive. Also make _processCropBorders idempotent by the existing _cropBorderCheckList guard --- lib/modules/manga/reader/reader_view.dart | 39 ++++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 36fdcdf0..5a9fa660 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -344,14 +344,10 @@ class _MangaChapterPageGalleryState Widget build(BuildContext context) { final backgroundColor = ref.watch(backgroundColorStateProvider); final fullScreenReader = ref.watch(fullScreenReaderStateProvider); - final cropBorders = ref.watch(cropBordersStateProvider); final readerMode = ref.watch(_currentReaderMode); final bool isHorizontalContinuous = readerMode == ReaderMode.horizontalContinuous || readerMode == ReaderMode.horizontalContinuousRTL; - if (cropBorders) { - _processCropBorders(); - } final l10n = l10nLocalizations(context)!; return ReaderKeyboardHandler( @@ -1148,13 +1144,17 @@ class _MangaChapterPageGalleryState } void _initCurrentIndex() async { + if (ref.read(cropBordersStateProvider)) _processCropBorders(); final readerMode = _readerController.getReaderMode(); // Initialize the preload manager with bounded memory (from ReaderMemoryManagement mixin) initializePreloadManager( _chapterUrlModel, onPagesUpdated: () { - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + if (ref.read(cropBordersStateProvider)) _processCropBorders(); + } }, ); @@ -1372,20 +1372,29 @@ class _MangaChapterPageGalleryState } } + bool _isCropBordersProcessing = false; void _processCropBorders() async { - if (_cropBorderCheckList.length == pages.length) return; + if (_isCropBordersProcessing || + _cropBorderCheckList.length == pages.length) { + return; + } + _isCropBordersProcessing = true; - for (var i = 0; i < pages.length; i++) { - if (!_cropBorderCheckList.contains(i)) { - _cropBorderCheckList.add(i); - if (!mounted) return; - final value = await ref.read( - cropBordersProvider(data: pages[i], cropBorder: true).future, - ); - if (mounted) { - updatePageCropImage(i, value); + try { + for (var i = 0; i < pages.length; i++) { + if (!_cropBorderCheckList.contains(i)) { + _cropBorderCheckList.add(i); + if (!mounted) return; + final value = await ref.read( + cropBordersProvider(data: pages[i], cropBorder: true).future, + ); + if (mounted) { + updatePageCropImage(i, value); + } } } + } finally { + _isCropBordersProcessing = false; } } From b0b48403fd310620a078b19f3b57e4ea03038ef2 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:47:46 +0200 Subject: [PATCH 08/23] Reduce FilterQuality reduce FilterQuality during scroll/zoom for smoother rendering --- .../manga/reader/image_view_vertical.dart | 163 +++++++++--------- .../manga/reader/image_view_webtoon.dart | 3 + lib/modules/manga/reader/reader_view.dart | 10 ++ 3 files changed, 98 insertions(+), 78 deletions(-) diff --git a/lib/modules/manga/reader/image_view_vertical.dart b/lib/modules/manga/reader/image_view_vertical.dart index f54e41cb..b87220aa 100644 --- a/lib/modules/manga/reader/image_view_vertical.dart +++ b/lib/modules/manga/reader/image_view_vertical.dart @@ -14,6 +14,7 @@ class ImageViewVertical extends ConsumerWidget { final UChapDataPreload data; final Function(UChapDataPreload data) onLongPressData; final bool isHorizontal; + final ValueNotifier isScrolling; final Function(bool) failedToLoadImage; @@ -23,94 +24,100 @@ class ImageViewVertical extends ConsumerWidget { required this.onLongPressData, required this.failedToLoadImage, required this.isHorizontal, + required this.isScrolling, }); @override Widget build(BuildContext context, WidgetRef ref) { final (colorBlendMode, color) = chapterColorFIlterValues(context, ref); - final imageWidget = ExtendedImage( - colorBlendMode: colorBlendMode, - color: color, - image: data.getImageProvider(ref, true), - filterQuality: FilterQuality.medium, - handleLoadingProgress: true, - fit: getBoxFit(ref.watch(scaleTypeStateProvider)), - enableLoadState: true, - loadStateChanged: (state) { - if (state.extendedImageLoadState == LoadState.completed) { - failedToLoadImage(false); - final rawSize = state.extendedImageInfo?.image; - if (rawSize != null && data.loadedHeight == null) { - final screenWidth = isHorizontal - ? context.width(0.8) - : MediaQuery.of(context).size.width; - final aspect = rawSize.width / rawSize.height; - data.loadedWidth = screenWidth; - data.loadedHeight = screenWidth / aspect; + final imageWidget = ValueListenableBuilder( + valueListenable: isScrolling, + builder: (context, scrolling, _) => ExtendedImage( + colorBlendMode: colorBlendMode, + color: color, + image: data.getImageProvider(ref, true), + filterQuality: scrolling ? FilterQuality.low : FilterQuality.medium, + handleLoadingProgress: true, + fit: getBoxFit(ref.watch(scaleTypeStateProvider)), + enableLoadState: true, + loadStateChanged: (state) { + if (state.extendedImageLoadState == LoadState.completed) { + failedToLoadImage(false); + final rawSize = state.extendedImageInfo?.image; + if (rawSize != null && data.loadedHeight == null) { + final screenWidth = isHorizontal + ? context.width(0.8) + : MediaQuery.of(context).size.width; + final aspect = rawSize.width / rawSize.height; + data.loadedWidth = screenWidth; + data.loadedHeight = screenWidth / aspect; + } } - } - final placeholderHeight = data.loadedHeight ?? context.height(0.8); - final placeholderWidth = isHorizontal - ? (data.loadedWidth ?? context.width(0.8)) - : null; - if (state.extendedImageLoadState == LoadState.loading) { - final ImageChunkEvent? loadingProgress = state.loadingProgress; - final double progress = loadingProgress?.expectedTotalBytes != null - ? loadingProgress!.cumulativeBytesLoaded / - loadingProgress.expectedTotalBytes! - : 0; - return Container( - color: Colors.black, - height: placeholderHeight, - width: placeholderWidth, - child: CircularProgressIndicatorAnimateRotate(progress: progress), - ); - } - if (state.extendedImageLoadState == LoadState.failed) { - failedToLoadImage(true); - return Container( - color: Colors.black, - height: placeholderHeight, - width: placeholderWidth, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - context.l10n.image_loading_error, - style: TextStyle(color: Colors.white.withValues(alpha: 0.7)), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: GestureDetector( - onLongPress: () { - state.reLoadImage(); - failedToLoadImage(false); - }, - onTap: () { - state.reLoadImage(); - failedToLoadImage(false); - }, - child: Container( - decoration: BoxDecoration( - color: context.primaryColor, - borderRadius: BorderRadius.circular(30), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 16, + final placeholderHeight = data.loadedHeight ?? context.height(0.8); + final placeholderWidth = isHorizontal + ? (data.loadedWidth ?? context.width(0.8)) + : null; + if (state.extendedImageLoadState == LoadState.loading) { + final ImageChunkEvent? loadingProgress = state.loadingProgress; + final double progress = loadingProgress?.expectedTotalBytes != null + ? loadingProgress!.cumulativeBytesLoaded / + loadingProgress.expectedTotalBytes! + : 0; + return Container( + color: Colors.black, + height: placeholderHeight, + width: placeholderWidth, + child: CircularProgressIndicatorAnimateRotate(progress: progress), + ); + } + if (state.extendedImageLoadState == LoadState.failed) { + failedToLoadImage(true); + return Container( + color: Colors.black, + height: placeholderHeight, + width: placeholderWidth, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.l10n.image_loading_error, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.7), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: GestureDetector( + onLongPress: () { + state.reLoadImage(); + failedToLoadImage(false); + }, + onTap: () { + state.reLoadImage(); + failedToLoadImage(false); + }, + child: Container( + decoration: BoxDecoration( + color: context.primaryColor, + borderRadius: BorderRadius.circular(30), + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16, + ), + child: Text(context.l10n.retry), ), - child: Text(context.l10n.retry), ), ), ), - ), - ], - ), - ); - } - return null; - }, + ], + ), + ); + } + return null; + }, + ), ); return applyReaderColorFilter( GestureDetector( diff --git a/lib/modules/manga/reader/image_view_webtoon.dart b/lib/modules/manga/reader/image_view_webtoon.dart index 14f3df19..54bf1353 100644 --- a/lib/modules/manga/reader/image_view_webtoon.dart +++ b/lib/modules/manga/reader/image_view_webtoon.dart @@ -34,6 +34,7 @@ class ImageViewWebtoon extends StatelessWidget { final int webtoonSidePadding; final bool showPageGaps; final bool reverse; + final ValueNotifier isScrolling; const ImageViewWebtoon({ super.key, @@ -57,6 +58,7 @@ class ImageViewWebtoon extends StatelessWidget { required this.onScaleEnd, required this.onDoubleTapDown, required this.onDoubleTap, + required this.isScrolling, this.webtoonSidePadding = 0, this.showPageGaps = true, this.reverse = false, @@ -130,6 +132,7 @@ class ImageViewWebtoon extends StatelessWidget { failedToLoadImage: onFailedToLoadImage, onLongPressData: onLongPressData, isHorizontal: isHorizontalContinuous, + isScrolling: isScrolling, ), ), ); diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 5a9fa660..2b942d72 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -152,6 +152,8 @@ class _MangaChapterPageGalleryState ); bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows; + final ValueNotifier _isScrolling = ValueNotifier(false); + Timer? _scrollIdleTimer; final Stopwatch _readingStopwatch = Stopwatch(); @@ -174,6 +176,8 @@ class _MangaChapterPageGalleryState _autoScroll.value = false; _autoScroll.dispose(); _autoScrollPage.dispose(); + _scrollIdleTimer?.cancel(); + _isScrolling.dispose(); _itemPositionsListener.itemPositions.removeListener(_readProgressListener); _photoViewController.dispose(); _photoViewScaleStateController.dispose(); @@ -458,6 +462,7 @@ class _MangaChapterPageGalleryState ), showPageGaps: ref.watch(showPageGapsStateProvider), reverse: _isReverseHorizontal, + isScrolling: _isScrolling, ) : Material( color: getBackgroundColor(backgroundColor), @@ -956,6 +961,11 @@ class _MangaChapterPageGalleryState final itemPositions = _itemPositionsListener.itemPositions.value; if (itemPositions.isEmpty) return; _currentIndex = itemPositions.first.index; + if (!_isScrolling.value) _isScrolling.value = true; + _scrollIdleTimer?.cancel(); + _scrollIdleTimer = Timer(const Duration(milliseconds: 150), () { + if (mounted) _isScrolling.value = false; + }); int pagesLength = (_pageMode == PageMode.doublePage && !(ref.watch(_currentReaderMode) == From 7f12e1ae6eff9307c14737b59751696dd7af51a7 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:34:20 +0200 Subject: [PATCH 09/23] cache settings --- .../providers/reader_controller_provider.dart | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index 2c4c35d2..11760301 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -67,6 +67,9 @@ class ReaderController extends _$ReaderController { } final incognitoMode = isar.settings.getSync(227)!.incognitoMode!; + Settings? _cachedSettings; + void _invalidateSettingsCache() => _cachedSettings = null; + ReaderMode getReaderMode() { final personalReaderModeList = getIsarSetting().personalReaderModeList ?? []; @@ -113,6 +116,7 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); + _invalidateSettingsCache(); } PageMode getPageMode() { @@ -146,6 +150,7 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); + _invalidateSettingsCache(); } void setPageMode(PageMode newPageMode) { @@ -167,6 +172,7 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); + _invalidateSettingsCache(); } void setShowPageNumber(bool value) { @@ -178,17 +184,14 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); + _invalidateSettingsCache(); } } - Settings getIsarSetting() { - return isar.settings.getSync(227)!; - } + Settings getIsarSetting() => _cachedSettings ??= isar.settings.getSync(227)!; bool getShowPageNumber() { - if (!incognitoMode) { - return getIsarSetting().showPagesNumber!; - } + if (!incognitoMode) return getIsarSetting().showPagesNumber!; return true; } @@ -389,6 +392,7 @@ class ReaderController extends _$ReaderController { chap.updatedAt = DateTime.now().millisecondsSinceEpoch; isar.chapters.putSync(chap); }); + _invalidateSettingsCache(); if (isRead) { chapter.updateTrackChapterRead(ref); if (ref.read(deleteDownloadAfterReadingStateProvider)) { @@ -471,10 +475,9 @@ extension ChapterExtensions on Chapter { extension MangaExtensions on Manga { List getFilteredChapterList() { final data = this.chapters.toList().reversed.toList(); + final settings = isar.settings.getSync(227)!; final filterUnread = - (isar.settings - .getSync(227)! - .chapterFilterUnreadList! + (settings.chapterFilterUnreadList! .where((element) => element.mangaId == id) .toList() .firstOrNull ?? @@ -482,18 +485,14 @@ extension MangaExtensions on Manga { .type!; final filterBookmarked = - (isar.settings - .getSync(227)! - .chapterFilterBookmarkedList! + (settings.chapterFilterBookmarkedList! .where((element) => element.mangaId == id) .toList() .firstOrNull ?? ChapterFilterBookmarked(mangaId: id, type: 0)) .type!; final filterDownloaded = - (isar.settings - .getSync(227)! - .chapterFilterDownloadedList! + (settings.chapterFilterDownloadedList! .where((element) => element.mangaId == id) .toList() .firstOrNull ?? @@ -501,9 +500,7 @@ extension MangaExtensions on Manga { .type!; final sortChapter = - (isar.settings - .getSync(227)! - .sortChapterList! + (settings.sortChapterList! .where((element) => element.mangaId == id) .toList() .firstOrNull ?? From c468de6a167b4b9eda06d36fc845ce0351a349a0 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:34:55 +0200 Subject: [PATCH 10/23] make code more readable --- lib/modules/manga/reader/reader_view.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 2b942d72..a3300a85 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -966,12 +966,11 @@ class _MangaChapterPageGalleryState _scrollIdleTimer = Timer(const Duration(milliseconds: 150), () { if (mounted) _isScrolling.value = false; }); + final currentReaderMode = ref.read(_currentReaderMode); int pagesLength = (_pageMode == PageMode.doublePage && - !(ref.watch(_currentReaderMode) == - ReaderMode.horizontalContinuous || - ref.watch(_currentReaderMode) == - ReaderMode.horizontalContinuousRTL)) + currentReaderMode != ReaderMode.horizontalContinuous && + currentReaderMode != ReaderMode.horizontalContinuousRTL) ? (pages.length / 2).ceil() : pages.length; if (_currentIndex! >= 0 && _currentIndex! < pagesLength) { From 1286afb3dfcb75ef892da8efa07366eea6585a03 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:35:06 +0200 Subject: [PATCH 11/23] change ref.watch to ref.read --- lib/modules/manga/reader/reader_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index a3300a85..b4a0fc54 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -1479,7 +1479,7 @@ class _MangaChapterPageGalleryState _isDoublePageActive ? (pages.length / 2).ceil() : pages.length; bool _isContinuousMode() { - final readerMode = ref.watch(_currentReaderMode); + final readerMode = ref.read(_currentReaderMode); return readerMode == ReaderMode.verticalContinuous || readerMode == ReaderMode.webtoon || readerMode == ReaderMode.horizontalContinuous || From 547413cad3f0b54b4f43c63b570bc10972c3e9e1 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:31:53 +0200 Subject: [PATCH 12/23] return correct List --- .../manga/reader/providers/reader_controller_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index 11760301..f1cdb4b7 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -564,7 +564,7 @@ extension MangaExtensions on Manga { : a.name!.compareTo(b.name!); }); } - return chapterList; + return chapters; } } From 9f113f295666dfe052e979be5336a77200d6dba8 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 15:27:50 +0200 Subject: [PATCH 13/23] =?UTF-8?q?Optimize=20downloaded=E2=80=91chapter=20f?= =?UTF-8?q?iltering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Precompute downloaded chapter IDs in a single query - Replace per‑chapter lookups with in‑memory `Set` checks --- .../providers/reader_controller_provider.dart | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index f1cdb4b7..d80275be 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -507,6 +507,16 @@ extension MangaExtensions on Manga { SortChapter(mangaId: id, index: 1, reverse: false)) .index; final filterScanlator = _getFilterScanlator(this) ?? []; + final chapterIds = data.map((c) => c.id).whereType().toList(); + final downloadedIds = (filterDownloaded == 0 || chapterIds.isEmpty) + ? const {} + : isar.downloads + .filter() + .anyOf(chapterIds, (q, id) => q.idEqualTo(id)) + .isDownloadEqualTo(true) + .findAllSync() + .map((d) => d.id!) + .toSet(); List? chapterList; chapterList = data .where( @@ -524,17 +534,9 @@ extension MangaExtensions on Manga { : true, ) .where((element) { - final modelChapDownload = isar.downloads - .filter() - .idEqualTo(element.id) - .findAllSync(); - return filterDownloaded == 1 - ? modelChapDownload.isNotEmpty && - modelChapDownload.first.isDownload == true - : filterDownloaded == 2 - ? !(modelChapDownload.isNotEmpty && - modelChapDownload.first.isDownload == true) - : true; + if (filterDownloaded == 0) return true; + final isDownloaded = downloadedIds.contains(element.id); + return filterDownloaded == 1 ? isDownloaded : !isDownloaded; }) .where((element) => !filterScanlator.contains(element.scanlator)) .toList(); From d8ff9fb01de658def47f121e765f81e4257226ee Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:39:47 +0200 Subject: [PATCH 14/23] performance+ Change Clip.antiAliasWithSaveLayer to Clip.antiAlias --- lib/modules/anime/widgets/search_subtitles.dart | 4 ++-- lib/modules/library/widgets/library_listview_widget.dart | 2 +- lib/modules/manga/detail/manga_detail_view.dart | 2 +- lib/modules/manga/detail/widgets/tracker_search_widget.dart | 5 ++--- .../updates/widgets/update_chapter_list_tile_widget.dart | 2 +- lib/modules/widgets/cover_view_widget.dart | 2 +- lib/modules/widgets/manga_image_card_widget.dart | 4 ++-- 7 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/modules/anime/widgets/search_subtitles.dart b/lib/modules/anime/widgets/search_subtitles.dart index f1c00820..112a6db7 100644 --- a/lib/modules/anime/widgets/search_subtitles.dart +++ b/lib/modules/anime/widgets/search_subtitles.dart @@ -76,7 +76,7 @@ class _SubtitlesWidgetSearchState extends ConsumerState { bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), ), - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: _isLoading ? SizedBox( height: context.height(0.3), @@ -229,7 +229,7 @@ class _SubtitlesWidgetSearchState extends ConsumerState { Material( borderRadius: BorderRadius.circular(5), color: Colors.transparent, - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: Ink.image( height: 120, width: 80, diff --git a/lib/modules/library/widgets/library_listview_widget.dart b/lib/modules/library/widgets/library_listview_widget.dart index 93e4d492..b40b8b4c 100644 --- a/lib/modules/library/widgets/library_listview_widget.dart +++ b/lib/modules/library/widgets/library_listview_widget.dart @@ -49,7 +49,7 @@ class LibraryListViewWidget extends StatelessWidget { return Material( borderRadius: BorderRadius.circular(5), color: Colors.transparent, - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: InkWell( onTap: () async { if (isLongPressed) { diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 75455c6d..93df9b56 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -2509,7 +2509,7 @@ class _MangaDetailViewState extends ConsumerState child: Material( color: bgColor, borderRadius: BorderRadius.circular(20), - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: Padding( padding: const EdgeInsets.all(8.0), child: SuperListView.separated( diff --git a/lib/modules/manga/detail/widgets/tracker_search_widget.dart b/lib/modules/manga/detail/widgets/tracker_search_widget.dart index 96ad5229..6d3013ca 100644 --- a/lib/modules/manga/detail/widgets/tracker_search_widget.dart +++ b/lib/modules/manga/detail/widgets/tracker_search_widget.dart @@ -77,7 +77,7 @@ class _TrackerWidgetSearchState extends ConsumerState { bottomLeft: Radius.circular(20), bottomRight: Radius.circular(20), ), - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: _isLoading ? SizedBox( height: context.height(0.3), @@ -123,8 +123,7 @@ class _TrackerWidgetSearchState extends ConsumerState { 5, ), color: Colors.transparent, - clipBehavior: - Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: Ink.image( height: 120, width: 80, diff --git a/lib/modules/updates/widgets/update_chapter_list_tile_widget.dart b/lib/modules/updates/widgets/update_chapter_list_tile_widget.dart index 4ec472ae..fd73d74a 100644 --- a/lib/modules/updates/widgets/update_chapter_list_tile_widget.dart +++ b/lib/modules/updates/widgets/update_chapter_list_tile_widget.dart @@ -25,7 +25,7 @@ class UpdateChapterListTileWidget extends ConsumerWidget { return Material( borderRadius: BorderRadius.circular(5), color: Colors.transparent, - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: InkWell( onTap: () async { chapter.pushToReaderView(context, ignoreIsRead: true); diff --git a/lib/modules/widgets/cover_view_widget.dart b/lib/modules/widgets/cover_view_widget.dart index b93315fe..17375ec2 100644 --- a/lib/modules/widgets/cover_view_widget.dart +++ b/lib/modules/widgets/cover_view_widget.dart @@ -32,7 +32,7 @@ class CoverViewWidget extends StatelessWidget { child: Material( borderRadius: BorderRadius.circular(5), color: Colors.transparent, - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: InkWell( onTap: onTap, onLongPress: onLongPress, diff --git a/lib/modules/widgets/manga_image_card_widget.dart b/lib/modules/widgets/manga_image_card_widget.dart index cfd11afe..99c098cc 100644 --- a/lib/modules/widgets/manga_image_card_widget.dart +++ b/lib/modules/widgets/manga_image_card_widget.dart @@ -212,7 +212,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget { child: Material( borderRadius: BorderRadius.circular(5), color: Colors.transparent, - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: InkWell( onTap: () { pushToMangaReaderDetail( @@ -258,7 +258,7 @@ class MangaImageCardListTileWidget extends ConsumerWidget { Material( borderRadius: BorderRadius.circular(5), color: Colors.transparent, - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.antiAlias, child: Image( height: 55, width: 40, From c5c97d712be2af656396cb60fcf720d1465be1f5 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 16:42:02 +0200 Subject: [PATCH 15/23] debounce the search query --- lib/modules/library/library_screen.dart | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index 09689416..5a9ca8a4 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -1,5 +1,6 @@ // ignore_for_file: use_build_context_synchronously +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/main.dart'; @@ -54,6 +55,7 @@ class _LibraryScreenState extends ConsumerState final _textEditingController = TextEditingController(); TabController? tabBarController; int _tabIndex = 0; + Timer? _searchDebounce; @override void initState() { @@ -68,6 +70,7 @@ class _LibraryScreenState extends ConsumerState void dispose() { _textEditingController.dispose(); tabBarController?.dispose(); + _searchDebounce?.cancel; super.dispose(); } @@ -266,7 +269,15 @@ class _LibraryScreenState extends ConsumerState textEditingController: _textEditingController, onSearchToggle: () => setState(() => _isSearch = !_isSearch), - onSearchClear: () => setState(() {}), + onSearchClear: () { + _searchDebounce?.cancel(); + _searchDebounce = Timer( + const Duration(milliseconds: 300), + () { + if (mounted) setState(() {}); + }, + ); + }, onIgnoreFiltersChanged: (val) => setState(() => _ignoreFiltersOnSearch = val), vsync: this, @@ -346,7 +357,12 @@ class _LibraryScreenState extends ConsumerState ignoreFiltersOnSearch: _ignoreFiltersOnSearch, textEditingController: _textEditingController, onSearchToggle: () => setState(() => _isSearch = !_isSearch), - onSearchClear: () => setState(() {}), + onSearchClear: () { + _searchDebounce?.cancel(); + _searchDebounce = Timer(const Duration(milliseconds: 300), () { + if (mounted) setState(() {}); + }); + }, onIgnoreFiltersChanged: (val) => setState(() => _ignoreFiltersOnSearch = val), vsync: this, From 81123dc3cbec51fec855bcaaee76325fcf272a39 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:19:32 +0200 Subject: [PATCH 16/23] Change the MangasListStateProvider to Set MangasListState previously stored selected manga IDs as List. Every visible library card called .contains() on that list once per rebuild to determine its highlight state, making each check O(n) in the number of selected items. The provider's own update/selectAll/ selectSome methods also used .contains() and .remove() on a List. Change the state type to Set throughout, making all membership checks O(1). Updated all consumers: library_gridview_widget, library_listview_widget, library_app_bar, library_dialogs, and MangasSetIsReadState. --- .../providers/library_state_provider.dart | 41 +++++++------------ .../providers/library_state_provider.g.dart | 32 +++++++-------- .../library/widgets/library_app_bar.dart | 2 +- .../library/widgets/library_dialogs.dart | 17 ++++---- .../widgets/library_gridview_widget.dart | 2 +- .../widgets/library_listview_widget.dart | 2 +- .../providers/download_provider.g.dart | 2 +- .../reader_controller_provider.g.dart | 2 +- lib/services/get_chapter_pages.g.dart | 2 +- 9 files changed, 46 insertions(+), 56 deletions(-) diff --git a/lib/modules/library/providers/library_state_provider.dart b/lib/modules/library/providers/library_state_provider.dart index a2ec69e7..8f739452 100644 --- a/lib/modules/library/providers/library_state_provider.dart +++ b/lib/modules/library/providers/library_state_provider.dart @@ -902,51 +902,40 @@ class SortLibraryMangaState extends _$SortLibraryMangaState { @riverpod class MangasListState extends _$MangasListState { @override - List build() { - return []; - } + Set build() => {}; void update(Manga value) { - var newList = state.reversed.toList(); - if (newList.contains(value.id)) { - newList.remove(value.id); + var newSet = Set.from(state); + if (newSet.contains(value.id)) { + newSet.remove(value.id); } else { - newList.add(value.id!); + newSet.add(value.id!); } - if (newList.isEmpty) { + if (newSet.isEmpty) { ref.read(isLongPressedStateProvider.notifier).update(false); } - state = newList; + state = newSet; } - void selectAll(Manga value) { - var newList = state.reversed.toList(); - if (!newList.contains(value.id)) { - newList.add(value.id!); - } - - state = newList; - } + void selectAll(Manga value) => state = {...state, value.id!}; void selectSome(Manga value) { - var newList = state.reversed.toList(); - if (newList.contains(value.id)) { - newList.remove(value.id); + final newSet = Set.from(state); + if (newSet.contains(value.id)) { + newSet.remove(value.id); } else { - newList.add(value.id!); + newSet.add(value.id!); } - state = newList; + state = newSet; } - void clear() { - state = []; - } + void clear() => state = {}; } @riverpod class MangasSetIsReadState extends _$MangasSetIsReadState { @override - void build({required List mangaIds, required bool markAsRead}) {} + void build({required Set mangaIds, required bool markAsRead}) {} void set() { final allChapters = []; diff --git a/lib/modules/library/providers/library_state_provider.g.dart b/lib/modules/library/providers/library_state_provider.g.dart index e68428ae..0f942f9f 100644 --- a/lib/modules/library/providers/library_state_provider.g.dart +++ b/lib/modules/library/providers/library_state_provider.g.dart @@ -1823,7 +1823,7 @@ abstract class _$SortLibraryMangaState extends $Notifier { final mangasListStateProvider = MangasListStateProvider._(); final class MangasListStateProvider - extends $NotifierProvider> { + extends $NotifierProvider> { MangasListStateProvider._() : super( from: null, @@ -1843,27 +1843,27 @@ final class MangasListStateProvider MangasListState create() => MangasListState(); /// {@macro riverpod.override_with_value} - Override overrideWithValue(List value) { + Override overrideWithValue(Set value) { return $ProviderOverride( origin: this, - providerOverride: $SyncValueProvider>(value), + providerOverride: $SyncValueProvider>(value), ); } } -String _$mangasListStateHash() => r'bbd2e3600ec22a774b1774ae3c221815e52bfef6'; +String _$mangasListStateHash() => r'61c6477ea43c6113caa89ef13984cd4370d303ee'; -abstract class _$MangasListState extends $Notifier> { - List build(); +abstract class _$MangasListState extends $Notifier> { + Set build(); @$mustCallSuper @override void runBuild() { - final ref = this.ref as $Ref, List>; + final ref = this.ref as $Ref, Set>; final element = ref.element as $ClassProviderElement< - AnyNotifier, List>, - List, + AnyNotifier, Set>, + Set, Object?, Object? >; @@ -1878,7 +1878,7 @@ final class MangasSetIsReadStateProvider extends $NotifierProvider { MangasSetIsReadStateProvider._({ required MangasSetIsReadStateFamily super.from, - required ({List mangaIds, bool markAsRead}) super.argument, + required ({Set mangaIds, bool markAsRead}) super.argument, }) : super( retry: null, name: r'mangasSetIsReadStateProvider', @@ -1921,7 +1921,7 @@ final class MangasSetIsReadStateProvider } String _$mangasSetIsReadStateHash() => - r'2a1b1005e2ed5068d36188a3fb969d21b64bfef6'; + r'a2c64ecdf03b3d27282c63d8cadbc1cc44943e39'; final class MangasSetIsReadStateFamily extends $Family with @@ -1930,7 +1930,7 @@ final class MangasSetIsReadStateFamily extends $Family void, void, void, - ({List mangaIds, bool markAsRead}) + ({Set mangaIds, bool markAsRead}) > { MangasSetIsReadStateFamily._() : super( @@ -1942,7 +1942,7 @@ final class MangasSetIsReadStateFamily extends $Family ); MangasSetIsReadStateProvider call({ - required List mangaIds, + required Set mangaIds, required bool markAsRead, }) => MangasSetIsReadStateProvider._( argument: (mangaIds: mangaIds, markAsRead: markAsRead), @@ -1954,11 +1954,11 @@ final class MangasSetIsReadStateFamily extends $Family } abstract class _$MangasSetIsReadState extends $Notifier { - late final _$args = ref.$arg as ({List mangaIds, bool markAsRead}); - List get mangaIds => _$args.mangaIds; + late final _$args = ref.$arg as ({Set mangaIds, bool markAsRead}); + Set get mangaIds => _$args.mangaIds; bool get markAsRead => _$args.markAsRead; - void build({required List mangaIds, required bool markAsRead}); + void build({required Set mangaIds, required bool markAsRead}); @$mustCallSuper @override void runBuild() { diff --git a/lib/modules/library/widgets/library_app_bar.dart b/lib/modules/library/widgets/library_app_bar.dart index 181e9f1e..bc38b898 100644 --- a/lib/modules/library/widgets/library_app_bar.dart +++ b/lib/modules/library/widgets/library_app_bar.dart @@ -226,7 +226,7 @@ class LibraryAppBar extends ConsumerWidget implements PreferredSizeWidget { /// AppBar shown when items are long-pressed for bulk selection. class _SelectionAppBar extends ConsumerWidget { final ItemType itemType; - final List mangaIdsList; + final Set mangaIdsList; final List data; const _SelectionAppBar({ diff --git a/lib/modules/library/widgets/library_dialogs.dart b/lib/modules/library/widgets/library_dialogs.dart index 0009a579..cd4c3059 100644 --- a/lib/modules/library/widgets/library_dialogs.dart +++ b/lib/modules/library/widgets/library_dialogs.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; @@ -27,8 +28,8 @@ void showDeleteMangaDialog({ required WidgetRef ref, required ItemType itemType, }) { - List fromLibList = []; - List downloadedChapsList = []; + Set fromLibList = {}; + Set downloadedChapsList = {}; showDialog( context: context, builder: (context) { @@ -53,10 +54,10 @@ void showDeleteMangaDialog({ label: l10n.from_library, onTap: () { setState(() { - if (fromLibList == mangaIdsList) { - fromLibList = []; + if (setEquals(fromLibList, mangaIdsList)) { + fromLibList = {}; } else { - fromLibList = mangaIdsList; + fromLibList = {...mangaIdsList}; } }); }, @@ -68,10 +69,10 @@ void showDeleteMangaDialog({ : l10n.downloaded_episodes, onTap: () { setState(() { - if (downloadedChapsList == mangaIdsList) { - downloadedChapsList = []; + if (setEquals(downloadedChapsList, mangaIdsList)) { + downloadedChapsList = {}; } else { - downloadedChapsList = mangaIdsList; + downloadedChapsList = {...mangaIdsList}; } }); }, diff --git a/lib/modules/library/widgets/library_gridview_widget.dart b/lib/modules/library/widgets/library_gridview_widget.dart index 48335245..5e1b8d6d 100644 --- a/lib/modules/library/widgets/library_gridview_widget.dart +++ b/lib/modules/library/widgets/library_gridview_widget.dart @@ -24,7 +24,7 @@ import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart'; class LibraryGridViewWidget extends StatefulWidget { final bool isCoverOnlyGrid; final bool isComfortableGrid; - final List mangaIdsList; + final Set mangaIdsList; final List entriesManga; final bool language; final bool downloadedChapter; diff --git a/lib/modules/library/widgets/library_listview_widget.dart b/lib/modules/library/widgets/library_listview_widget.dart index b40b8b4c..e0c8777e 100644 --- a/lib/modules/library/widgets/library_listview_widget.dart +++ b/lib/modules/library/widgets/library_listview_widget.dart @@ -23,7 +23,7 @@ class LibraryListViewWidget extends StatelessWidget { final List entriesManga; final bool language; final bool downloadedChapter; - final List mangaIdsList; + final Set mangaIdsList; final bool continueReaderBtn; final bool localSource; const LibraryListViewWidget({ diff --git a/lib/modules/manga/download/providers/download_provider.g.dart b/lib/modules/manga/download/providers/download_provider.g.dart index afc5b46d..4320e028 100644 --- a/lib/modules/manga/download/providers/download_provider.g.dart +++ b/lib/modules/manga/download/providers/download_provider.g.dart @@ -136,7 +136,7 @@ final class DownloadChapterProvider } } -String _$downloadChapterHash() => r'690619b8914877f3913ed1601818b6149752279b'; +String _$downloadChapterHash() => r'c0d7bc9cd975bb5f1abdf29f9aa6d9d8dc8ca441'; final class DownloadChapterFamily extends $Family with diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart index 59bca3ce..22fa1e81 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart @@ -147,7 +147,7 @@ final class ReaderControllerProvider } } -String _$readerControllerHash() => r'fd5ce439786209d9e218fa4067f91f606bb8458a'; +String _$readerControllerHash() => r'adab728fa21939302d0f928b11be204e8e8a0527'; final class ReaderControllerFamily extends $Family with diff --git a/lib/services/get_chapter_pages.g.dart b/lib/services/get_chapter_pages.g.dart index 2ae69ef7..106fff8a 100644 --- a/lib/services/get_chapter_pages.g.dart +++ b/lib/services/get_chapter_pages.g.dart @@ -66,7 +66,7 @@ final class GetChapterPagesProvider } } -String _$getChapterPagesHash() => r'544311ac02b1034b938bb5f85e97fe34683c26c7'; +String _$getChapterPagesHash() => r'593f5af68761ff44d50fb3667d6717edf58769d7'; final class GetChapterPagesFamily extends $Family with $FunctionalFamilyOverride, Chapter> { From 31409bfb3caef818ad11c02bfcda51dbd4ac628d Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:20:39 +0200 Subject: [PATCH 17/23] Use mangaIds No need to create new `mangaIdsList` variable, because mangaIds is the same --- lib/modules/library/library_screen.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index 5a9ca8a4..71603005 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -452,8 +452,7 @@ class _LibraryScreenState extends ConsumerState BottomSelectButton( icon: Icon(Icons.label_outline_rounded, color: color), onPressed: () { - final mangaIdsList = ref.watch(mangasListStateProvider); - final List bulkMangas = mangaIdsList + final List bulkMangas = mangaIds .map((id) => isar.mangas.getSync(id)!) .toList(); showCategorySelectionDialog( From 3f19636fcf621e8613a003a62533e761717c3f34 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:27:56 +0200 Subject: [PATCH 18/23] Optimize download check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change per‑chapter synchronous lookups inside isar.txnSync to a single batched anyOf query using collected chapter IDs. Reduces repeated filtering, removes unnecessary loop, and improves performance. --- .../widgets/library_gridview_widget.dart | 36 ++++++++---------- .../widgets/library_listview_widget.dart | 38 ++++++++----------- 2 files changed, 31 insertions(+), 43 deletions(-) diff --git a/lib/modules/library/widgets/library_gridview_widget.dart b/lib/modules/library/widgets/library_gridview_widget.dart index 5e1b8d6d..16a2310d 100644 --- a/lib/modules/library/widgets/library_gridview_widget.dart +++ b/lib/modules/library/widgets/library_gridview_widget.dart @@ -178,27 +178,21 @@ class _LibraryGridViewWidgetState extends State { builder: (context, ref, child) { List nbrDown = []; if (widget.downloadedChapter) { - isar.txnSync(() { - for ( - var i = 0; - i < entry.chapters.length; - i++ - ) { - final entries = isar.downloads - .filter() - .idEqualTo( - entry.chapters - .toList()[i] - .id, - ) - .findAllSync(); - - if (entries.isNotEmpty && - entries.first.isDownload!) { - nbrDown.add(1); - } - } - }); + final chapterIds = entry.chapters + .toList() + .map((c) => c.id) + .whereType() + .toList(); + if (chapterIds.isNotEmpty) { + nbrDown = isar.downloads + .filter() + .anyOf( + chapterIds, + (q, id) => q.idEqualTo(id), + ) + .isDownloadEqualTo(true) + .findAllSync(); + } } return Row( diff --git a/lib/modules/library/widgets/library_listview_widget.dart b/lib/modules/library/widgets/library_listview_widget.dart index e0c8777e..6c2e240e 100644 --- a/lib/modules/library/widgets/library_listview_widget.dart +++ b/lib/modules/library/widgets/library_listview_widget.dart @@ -208,28 +208,22 @@ class LibraryListViewWidget extends StatelessWidget { ), child: Consumer( builder: (context, ref, child) { - List nbrDown = []; - isar.txnSync(() { - for ( - var i = 0; - i < entry.chapters.length; - i++ - ) { - final entries = isar.downloads - .filter() - .idEqualTo( - entry.chapters - .toList()[i] - .id, - ) - .findAllSync(); - - if (entries.isNotEmpty && - entries.first.isDownload!) { - nbrDown.add(entries.first); - } - } - }); + final chapterIds = entry.chapters + .toList() + .map((c) => c.id) + .whereType() + .toList(); + List nbrDown = chapterIds.isNotEmpty + ? isar.downloads + .filter() + .anyOf( + chapterIds, + (q, id) => + q.idEqualTo(id), + ) + .isDownloadEqualTo(true) + .findAllSync() + : []; if (nbrDown.isNotEmpty) { return Container( decoration: BoxDecoration( From d706e82fd867c61d8b2102c5c9eff9c9939dcb4c Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:05:55 +0200 Subject: [PATCH 19/23] simplify query for continueReaderBtn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor history lookup to use a direct mangaIdEqualTo query instead of the previous multi‑level chapter/manga filter. The new approach improves performance, readability, and maintainability without changing how the play button behaves or how history entries are resolved. --- .../widgets/library_gridview_widget.dart | 18 ++---------------- .../widgets/library_listview_widget.dart | 17 ++--------------- 2 files changed, 4 insertions(+), 31 deletions(-) diff --git a/lib/modules/library/widgets/library_gridview_widget.dart b/lib/modules/library/widgets/library_gridview_widget.dart index 16a2310d..9da4184f 100644 --- a/lib/modules/library/widgets/library_gridview_widget.dart +++ b/lib/modules/library/widgets/library_gridview_widget.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; -import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/modules/library/providers/isar_providers.dart'; @@ -194,7 +193,6 @@ class _LibraryGridViewWidgetState extends State { .findAllSync(); } } - return Row( children: [ if (nbrDown.isNotEmpty && @@ -302,14 +300,7 @@ class _LibraryGridViewWidgetState extends State { return StreamBuilder( stream: isar.historys .filter() - .idIsNotNull() - .and() - .chapter( - (q) => q.manga( - (q) => - q.itemTypeEqualTo(entry.itemType), - ), - ) + .mangaIdEqualTo(entry.id!) .watch(fireImmediately: true), builder: (context, snapshot) { if (snapshot.hasData && @@ -317,12 +308,7 @@ class _LibraryGridViewWidgetState extends State { final incognitoMode = ref.watch( incognitoModeStateProvider, ); - final entries = snapshot.data! - .where( - (element) => - element.mangaId == entry.id, - ) - .toList(); + final entries = snapshot.data!; if (entries.isNotEmpty && !incognitoMode) { return GestureDetector( diff --git a/lib/modules/library/widgets/library_listview_widget.dart b/lib/modules/library/widgets/library_listview_widget.dart index 6c2e240e..4de0da7d 100644 --- a/lib/modules/library/widgets/library_listview_widget.dart +++ b/lib/modules/library/widgets/library_listview_widget.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; -import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/modules/library/providers/isar_providers.dart'; @@ -306,14 +305,7 @@ class LibraryListViewWidget extends StatelessWidget { return StreamBuilder( stream: isar.historys .filter() - .idIsNotNull() - .and() - .chapter( - (q) => q.manga( - (q) => - q.itemTypeEqualTo(entry.itemType), - ), - ) + .mangaIdEqualTo(entry.id!) .watch(fireImmediately: true), builder: (context, snapshot) { if (snapshot.hasData && @@ -321,12 +313,7 @@ class LibraryListViewWidget extends StatelessWidget { final incognitoMode = ref.watch( incognitoModeStateProvider, ); - final entries = snapshot.data! - .where( - (element) => - element.mangaId == entry.id, - ) - .toList(); + final entries = snapshot.data!; if (entries.isNotEmpty && !incognitoMode) { final chap = From 05111da02bdf76a1468f03048b84b5a2d097ec38 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 18:47:25 +0200 Subject: [PATCH 20/23] Extract ContinueReaderButton --- .../widgets/continue_reader_button.dart | 82 ++++++++++++++ .../widgets/library_gridview_widget.dart | 103 +---------------- .../widgets/library_listview_widget.dart | 104 +----------------- 3 files changed, 86 insertions(+), 203 deletions(-) create mode 100644 lib/modules/library/widgets/continue_reader_button.dart diff --git a/lib/modules/library/widgets/continue_reader_button.dart b/lib/modules/library/widgets/continue_reader_button.dart new file mode 100644 index 00000000..25e75f9e --- /dev/null +++ b/lib/modules/library/widgets/continue_reader_button.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar_community/isar.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/history.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart'; +import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/extensions/chapter.dart'; + +class ContinueReaderButton extends ConsumerWidget { + final Manga entry; + + const ContinueReaderButton({super.key, required this.entry}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return StreamBuilder( + stream: isar.historys + .filter() + .mangaIdEqualTo(entry.id!) + .watch(fireImmediately: true), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + final incognitoMode = ref.read(incognitoModeStateProvider); + final entries = snapshot.data!; + if (entries.isNotEmpty && !incognitoMode) { + return GestureDetector( + onTap: () { + entries.first.chapter.value!.pushToReaderView(context); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: context.primaryColor.withValues(alpha: 0.9), + ), + child: const Padding( + padding: EdgeInsets.all(7), + child: Icon(Icons.play_arrow, size: 19, color: Colors.white), + ), + ), + ); + } + return GestureDetector( + onTap: () { + entry.chapters.toList().reversed.toList().last.pushToReaderView( + context, + ); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: context.primaryColor.withValues(alpha: 0.9), + ), + child: const Padding( + padding: EdgeInsets.all(7), + child: Icon(Icons.play_arrow, size: 19, color: Colors.white), + ), + ), + ); + } + return GestureDetector( + onTap: () { + entry.chapters.toList().reversed.toList().last.pushToReaderView( + context, + ); + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: context.primaryColor.withValues(alpha: 0.9), + ), + child: const Padding( + padding: EdgeInsets.all(7), + child: Icon(Icons.play_arrow, size: 19, color: Colors.white), + ), + ), + ); + }, + ); + } +} diff --git a/lib/modules/library/widgets/library_gridview_widget.dart b/lib/modules/library/widgets/library_gridview_widget.dart index 9da4184f..b6fff370 100644 --- a/lib/modules/library/widgets/library_gridview_widget.dart +++ b/lib/modules/library/widgets/library_gridview_widget.dart @@ -4,17 +4,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/download.dart'; -import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/modules/library/providers/isar_providers.dart'; import 'package:mangayomi/modules/library/providers/library_state_provider.dart'; import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/library/widgets/continue_reader_button.dart'; import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart'; import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/utils/constant.dart'; -import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/utils/headers.dart'; -import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart'; import 'package:mangayomi/modules/widgets/bottom_text_widget.dart'; import 'package:mangayomi/modules/widgets/cover_view_widget.dart'; import 'package:mangayomi/modules/widgets/gridview_widget.dart'; @@ -295,104 +293,7 @@ class _LibraryGridViewWidgetState extends State { right: 0, child: Padding( padding: const EdgeInsets.all(9), - child: Consumer( - builder: (context, ref, child) { - return StreamBuilder( - stream: isar.historys - .filter() - .mangaIdEqualTo(entry.id!) - .watch(fireImmediately: true), - builder: (context, snapshot) { - if (snapshot.hasData && - snapshot.data!.isNotEmpty) { - final incognitoMode = ref.watch( - incognitoModeStateProvider, - ); - final entries = snapshot.data!; - if (entries.isNotEmpty && - !incognitoMode) { - return GestureDetector( - onTap: () { - entries.first.chapter.value! - .pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(5), - color: context.primaryColor - .withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon( - Icons.play_arrow, - size: 19, - color: Colors.white, - ), - ), - ), - ); - } - return GestureDetector( - onTap: () { - entry.chapters - .toList() - .reversed - .toList() - .last - .pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 5, - ), - color: context.primaryColor - .withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon( - Icons.play_arrow, - size: 19, - color: Colors.white, - ), - ), - ), - ); - } - return GestureDetector( - onTap: () { - entry.chapters - .toList() - .reversed - .toList() - .last - .pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 5, - ), - color: context.primaryColor - .withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon( - Icons.play_arrow, - size: 19, - color: Colors.white, - ), - ), - ), - ); - }, - ); - }, - ), + child: ContinueReaderButton(entry: entry), ), ), ], diff --git a/lib/modules/library/widgets/library_listview_widget.dart b/lib/modules/library/widgets/library_listview_widget.dart index 4de0da7d..f7e557f7 100644 --- a/lib/modules/library/widgets/library_listview_widget.dart +++ b/lib/modules/library/widgets/library_listview_widget.dart @@ -4,17 +4,15 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/download.dart'; -import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/modules/library/providers/isar_providers.dart'; import 'package:mangayomi/modules/library/providers/library_state_provider.dart'; import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/library/widgets/continue_reader_button.dart'; import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart'; import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/utils/constant.dart'; -import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/utils/headers.dart'; -import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart'; import 'package:mangayomi/modules/widgets/listview_widget.dart'; import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart'; @@ -300,105 +298,7 @@ class LibraryListViewWidget extends StatelessWidget { ), ), if (continueReaderBtn) - Consumer( - builder: (context, ref, child) { - return StreamBuilder( - stream: isar.historys - .filter() - .mangaIdEqualTo(entry.id!) - .watch(fireImmediately: true), - builder: (context, snapshot) { - if (snapshot.hasData && - snapshot.data!.isNotEmpty) { - final incognitoMode = ref.watch( - incognitoModeStateProvider, - ); - final entries = snapshot.data!; - if (entries.isNotEmpty && - !incognitoMode) { - final chap = - entries.first.chapter.value!; - return GestureDetector( - onTap: () { - chap.pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(5), - color: context.primaryColor - .withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon( - Icons.play_arrow, - size: 19, - color: Colors.white, - ), - ), - ), - ); - } - return GestureDetector( - onTap: () { - entry.chapters - .toList() - .reversed - .toList() - .last - .pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 5, - ), - color: context.primaryColor - .withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon( - Icons.play_arrow, - size: 19, - color: Colors.white, - ), - ), - ), - ); - } - return GestureDetector( - onTap: () { - entry.chapters - .toList() - .reversed - .toList() - .last - .pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular( - 5, - ), - color: context.primaryColor - .withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon( - Icons.play_arrow, - size: 19, - color: Colors.white, - ), - ), - ), - ); - }, - ); - }, - ), + ContinueReaderButton(entry: entry), ], ), ), From cc05ee13e8cffaa4777aa391207f8be9bcb2241b Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:33:57 +0200 Subject: [PATCH 21/23] Reduce code duplication Also, change `entry.chapters.toList().reversed.toList().last` to `entry.chapters.first` Why reverse the list and then get the last item, when you can just get the first? --- .../widgets/continue_reader_button.dart | 71 +++++-------------- 1 file changed, 17 insertions(+), 54 deletions(-) diff --git a/lib/modules/library/widgets/continue_reader_button.dart b/lib/modules/library/widgets/continue_reader_button.dart index 25e75f9e..df1945e2 100644 --- a/lib/modules/library/widgets/continue_reader_button.dart +++ b/lib/modules/library/widgets/continue_reader_button.dart @@ -20,63 +20,26 @@ class ContinueReaderButton extends ConsumerWidget { .filter() .mangaIdEqualTo(entry.id!) .watch(fireImmediately: true), - builder: (context, snapshot) { - if (snapshot.hasData && snapshot.data!.isNotEmpty) { + builder: (context, snapshot) => GestureDetector( + onTap: () { final incognitoMode = ref.read(incognitoModeStateProvider); - final entries = snapshot.data!; - if (entries.isNotEmpty && !incognitoMode) { - return GestureDetector( - onTap: () { - entries.first.chapter.value!.pushToReaderView(context); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: context.primaryColor.withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon(Icons.play_arrow, size: 19, color: Colors.white), - ), - ), - ); + if (snapshot.hasData && snapshot.data!.isNotEmpty && !incognitoMode) { + snapshot.data!.first.chapter.value!.pushToReaderView(context); + } else { + entry.chapters.first.pushToReaderView(context); } - return GestureDetector( - onTap: () { - entry.chapters.toList().reversed.toList().last.pushToReaderView( - context, - ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: context.primaryColor.withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon(Icons.play_arrow, size: 19, color: Colors.white), - ), - ), - ); - } - return GestureDetector( - onTap: () { - entry.chapters.toList().reversed.toList().last.pushToReaderView( - context, - ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5), - color: context.primaryColor.withValues(alpha: 0.9), - ), - child: const Padding( - padding: EdgeInsets.all(7), - child: Icon(Icons.play_arrow, size: 19, color: Colors.white), - ), + }, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: context.primaryColor.withValues(alpha: 0.9), ), - ); - }, + child: const Padding( + padding: EdgeInsets.all(7), + child: Icon(Icons.play_arrow, size: 19, color: Colors.white), + ), + ), + ), ); } } From e33af9cbe678bda34ed57551fd4da59a6e641f7e Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:54:30 +0200 Subject: [PATCH 22/23] Update library_screen.dart --- lib/modules/library/library_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index 71603005..758542fb 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -70,7 +70,7 @@ class _LibraryScreenState extends ConsumerState void dispose() { _textEditingController.dispose(); tabBarController?.dispose(); - _searchDebounce?.cancel; + _searchDebounce?.cancel(); super.dispose(); } From feb0a3635f9bff36ec8245cd0fd3be5ee807dafb Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Mon, 13 Apr 2026 10:40:05 +0100 Subject: [PATCH 23/23] Fix sorting logic for chapters --- .../manga/reader/providers/reader_controller_provider.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index d80275be..1869c9e8 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -540,7 +540,7 @@ extension MangaExtensions on Manga { }) .where((element) => !filterScanlator.contains(element.scanlator)) .toList(); - List chapters = sortChapter == 1 + List chapters = sortChapter == 0 ? chapterList.reversed.toList() : chapterList; if (sortChapter == 0) {