From 35479187ca26c70c807e71dd5f538e16a91f38df Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:41:17 +0200 Subject: [PATCH 01/20] Remove unused import --- lib/modules/mass_migration/services/mass_migration_service.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/modules/mass_migration/services/mass_migration_service.dart b/lib/modules/mass_migration/services/mass_migration_service.dart index b3eaf134..0670c2dc 100644 --- a/lib/modules/mass_migration/services/mass_migration_service.dart +++ b/lib/modules/mass_migration/services/mass_migration_service.dart @@ -1,5 +1,3 @@ -import 'dart:collection'; - import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/eval/model/m_manga.dart'; From c2d19fd17dd5a02e032f32bfc60d6253905ecfe3 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Mon, 13 Apr 2026 02:00:12 +0200 Subject: [PATCH 02/20] make the list const --- lib/modules/browse/global_search/global_search_screen.dart | 2 +- lib/modules/manga/detail/widgets/migrate_screen.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modules/browse/global_search/global_search_screen.dart b/lib/modules/browse/global_search/global_search_screen.dart index c6da3c57..8e2e00d1 100644 --- a/lib/modules/browse/global_search/global_search_screen.dart +++ b/lib/modules/browse/global_search/global_search_screen.dart @@ -156,7 +156,7 @@ class _SourceSearchScreenState extends ConsumerState { source: widget.source, page: 1, query: widget.query, - filterList: [], + filterList: const [], ).future, ); if (mounted) { diff --git a/lib/modules/manga/detail/widgets/migrate_screen.dart b/lib/modules/manga/detail/widgets/migrate_screen.dart index 947e7813..2d5a3bab 100644 --- a/lib/modules/manga/detail/widgets/migrate_screen.dart +++ b/lib/modules/manga/detail/widgets/migrate_screen.dart @@ -173,7 +173,7 @@ class _MigrationSourceSearchScreenState source: widget.source, page: 1, query: widget.query, - filterList: [], + filterList: const [], ).future, ); if (mounted) { From 63e747fa3ea79fbe939a77e4cb3f6d606da7d4ce Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Mon, 13 Apr 2026 21:04:48 +0200 Subject: [PATCH 03/20] watch -> read --- lib/modules/more/about/providers/check_for_update.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modules/more/about/providers/check_for_update.dart b/lib/modules/more/about/providers/check_for_update.dart index 099b0086..1f80d49d 100644 --- a/lib/modules/more/about/providers/check_for_update.dart +++ b/lib/modules/more/about/providers/check_for_update.dart @@ -21,7 +21,7 @@ Future checkForUpdate( bool? manualUpdate, }) async { manualUpdate = manualUpdate ?? false; - final checkForUpdates = ref.watch(checkForAppUpdatesProvider); + final checkForUpdates = ref.read(checkForAppUpdatesProvider); if (!checkForUpdates && !manualUpdate) return; final l10n = l10nLocalizations(context!)!; From 04e04010f4f4b50cfb31fd524a8a8e2fcb5752bc Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:16:49 +0200 Subject: [PATCH 04/20] Reduce Code Duplication --- lib/modules/anime/anime_player_view.dart | 10 +- .../anime_player_controller_provider.dart | 160 +++--------- .../mixins/chapter_controller_mixin.dart | 134 ++++++++++ .../mixins/chapter_reader_settings_mixin.dart | 108 ++++++++ .../providers/reader_controller_provider.dart | 233 ++---------------- lib/modules/manga/reader/reader_view.dart | 6 +- .../novel_reader_controller_provider.dart | 218 ++-------------- lib/modules/novel/novel_reader_view.dart | 4 +- 8 files changed, 324 insertions(+), 549 deletions(-) create mode 100644 lib/modules/manga/reader/mixins/chapter_controller_mixin.dart create mode 100644 lib/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart diff --git a/lib/modules/anime/anime_player_view.dart b/lib/modules/anime/anime_player_view.dart index 1ffd5fbe..acb74d19 100644 --- a/lib/modules/anime/anime_player_view.dart +++ b/lib/modules/anime/anime_player_view.dart @@ -279,7 +279,7 @@ class _AnimeStreamPageState extends riv.ConsumerState final ValueNotifier _playbackSpeed = ValueNotifier(1.0); final ValueNotifier _isDoubleSpeed = ValueNotifier(false); late final ValueNotifier _currentPosition = ValueNotifier( - _streamController.geTCurrentPosition(), + _streamController.getCurrentPosition(), ); final ValueNotifier _currentTotalDuration = ValueNotifier(null); final ValueNotifier _showFitLabel = ValueNotifier(false); @@ -899,11 +899,11 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo _completed; _currentTotalDurationSub; _loadAndroidFont().then((_) { - _openMedia(_video.value!, _streamController.geTCurrentPosition()); + _openMedia(_video.value!, _streamController.getCurrentPosition()); if (widget.isTorrent) { Future.delayed(const Duration(seconds: 10)).then((_) { if (mounted) { - _openMedia(_video.value!, _streamController.geTCurrentPosition()); + _openMedia(_video.value!, _streamController.getCurrentPosition()); } }); } @@ -1020,8 +1020,8 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo _currentTotalDuration.value, save: save, ); - _streamController.setAnimeHistoryUpdate( - watchTimeSeconds: saveWatchTime ? _watchStopwatch.elapsed.inSeconds : 0, + _streamController.setHistoryUpdate( + elapsedSeconds: saveWatchTime ? _watchStopwatch.elapsed.inSeconds : 0, ); } diff --git a/lib/modules/anime/providers/anime_player_controller_provider.dart b/lib/modules/anime/providers/anime_player_controller_provider.dart index a4694285..6af26c78 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.dart @@ -2,10 +2,9 @@ import 'package:flutter_riverpod/misc.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/models/history.dart'; -import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/track.dart'; +import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart'; import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart'; import 'package:mangayomi/services/aniskip.dart'; @@ -17,7 +16,8 @@ part 'anime_player_controller_provider.g.dart'; final fullscreenProvider = StateProvider(() => false); @riverpod -class AnimeStreamController extends _$AnimeStreamController { +class AnimeStreamController extends _$AnimeStreamController + with ChapterControllerMixin { @override KeepAliveLink build({required Chapter episode}) { _keepAliveLink = ref.keepAlive(); @@ -25,101 +25,37 @@ class AnimeStreamController extends _$AnimeStreamController { } KeepAliveLink? _keepAliveLink; - KeepAliveLink? get keepAliveLink => _keepAliveLink; - Manga getAnime() { - return episode.manga.value!; - } - final incognitoMode = isar.settings.getSync(227)!.incognitoMode!; + // Bridge the mixin's `chapter` contract to the `episode` build parameter. + @override + Chapter get chapter => episode; - Settings getIsarSetting() { - return isar.settings.getSync(227)!; - } + // Keep incognitoMode as a final field (read once, not on every access). + @override + final bool incognitoMode = isar.settings.getSync(227)!.incognitoMode!; - (int, bool) getEpisodeIndex() { - final episodes = getAnime().getFilteredChapterList(); - int? index; - for (var i = 0; i < episodes.length; i++) { - if (episodes[i].id == episode.id) { - index = i; - } - } - if (index == null) { - final episodes = getAnime().chapters.toList().reversed.toList(); - for (var i = 0; i < episodes.length; i++) { - if (episodes[i].id == episode.id) { - index = i; - } - } - return (index!, false); - } - return (index, true); - } + // --------------------------------------------------------------------------- + // Anime-flavoured aliases (preserve the existing public API) + // --------------------------------------------------------------------------- - (int, bool) getPrevEpisodeIndex() { - final episodes = getAnime().getFilteredChapterList(); - int? index; - for (var i = 0; i < episodes.length; i++) { - if (episodes[i].id == episode.id) { - index = i + 1; - } - } - if (index == null) { - final episodes = getAnime().chapters.toList().reversed.toList(); - for (var i = 0; i < episodes.length; i++) { - if (episodes[i].id == episode.id) { - index = i + 1; - } - } - return (index!, false); - } - return (index, true); - } + (int, bool) getEpisodeIndex() => getChapterIndex(); + (int, bool) getPrevEpisodeIndex() => getPrevChapterIndex(); + (int, bool) getNextEpisodeIndex() => getNextChapterIndex(); - (int, bool) getNextEpisodeIndex() { - final episodes = getAnime().getFilteredChapterList(); - int? index; - for (var i = 0; i < episodes.length; i++) { - if (episodes[i].id == episode.id) { - index = i - 1; - } - } - if (index == null) { - final episodes = getAnime().chapters.toList().reversed.toList(); - for (var i = 0; i < episodes.length; i++) { - if (episodes[i].id == episode.id) { - index = i - 1; - } - } - return (index!, false); - } - return (index, true); - } + Chapter getPrevEpisode() => getPrevChapter(); + Chapter getNextEpisode() => getNextChapter(); - Chapter getPrevEpisode() { - final prevEpIdx = getPrevEpisodeIndex(); - return prevEpIdx.$2 - ? getAnime().getFilteredChapterList()[prevEpIdx.$1] - : getAnime().chapters.toList().reversed.toList()[prevEpIdx.$1]; - } + int getEpisodesLength(bool isInFilterList) => + getChaptersLength(isInFilterList); - Chapter getNextEpisode() { - final nextEpIdx = getNextEpisodeIndex(); - return nextEpIdx.$2 - ? getAnime().getFilteredChapterList()[nextEpIdx.$1] - : getAnime().chapters.toList().reversed.toList()[nextEpIdx.$1]; - } + // --------------------------------------------------------------------------- + // Playback position + // --------------------------------------------------------------------------- - int getEpisodesLength(bool isInFilterList) { - return isInFilterList - ? getAnime().getFilteredChapterList().length - : getAnime().chapters.length; - } - - Duration geTCurrentPosition() { + Duration getCurrentPosition() { if (incognitoMode) return Duration.zero; - String position = episode.lastPageRead ?? "0"; + final position = episode.lastPageRead ?? '0'; return Duration( milliseconds: episode.isRead! ? 0 @@ -127,50 +63,6 @@ class AnimeStreamController extends _$AnimeStreamController { ); } - void setAnimeHistoryUpdate({int watchTimeSeconds = 0}) { - if (incognitoMode) return; - isar.writeTxnSync(() { - Manga? anime = episode.manga.value; - anime!.lastRead = DateTime.now().millisecondsSinceEpoch; - anime.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.mangas.putSync(anime); - }); - History? history; - - final empty = isar.historys - .filter() - .mangaIdEqualTo(getAnime().id) - .isEmptySync(); - - if (empty) { - history = History( - mangaId: getAnime().id, - date: DateTime.now().millisecondsSinceEpoch.toString(), - itemType: getAnime().itemType, - chapterId: episode.id, - updatedAt: DateTime.now().millisecondsSinceEpoch, - )..chapter.value = episode; - } else { - history = - (isar.historys - .filter() - .mangaIdEqualTo(getAnime().id) - .findFirstSync())! - ..chapterId = episode.id - ..chapter.value = episode - ..date = DateTime.now().millisecondsSinceEpoch.toString() - ..updatedAt = DateTime.now().millisecondsSinceEpoch; - } - isar.writeTxnSync(() { - if (watchTimeSeconds > 0) { - history!.readingTimeSeconds = - (history.readingTimeSeconds ?? 0) + watchTimeSeconds; - } - isar.historys.putSync(history!); - history.chapter.saveSync(); - }); - } - void setCurrentPosition( Duration duration, Duration? totalDuration, { @@ -200,6 +92,10 @@ class AnimeStreamController extends _$AnimeStreamController { } } + // --------------------------------------------------------------------------- + // AniSkip + // --------------------------------------------------------------------------- + (int, int)? _getTrackId() { final malId = isar.tracks .filter() diff --git a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart new file mode 100644 index 00000000..d53c329f --- /dev/null +++ b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart @@ -0,0 +1,134 @@ +import 'package:isar_community/isar.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/history.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/models/settings.dart'; +import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; + +/// Shared navigation and history logic used by [ReaderController], +/// [NovelReaderController], and [AnimeStreamController]. +/// +/// Concrete classes must satisfy the single abstract member [chapter]. +/// The Riverpod-generated base class already exposes the build parameter as a +/// getter, so no extra boilerplate is needed in normal cases. +/// +/// [incognitoMode] and [getIsarSetting] are concrete in the mixin but can be +/// overridden — [ReaderController] overrides [getIsarSetting] to add caching. +mixin ChapterControllerMixin { + // --------------------------------------------------------------------------- + // Contract – provided by the Riverpod-generated superclass + // --------------------------------------------------------------------------- + + /// The current chapter or episode. + Chapter get chapter; + + // --------------------------------------------------------------------------- + // Basic helpers + // --------------------------------------------------------------------------- + + Manga getManga() => chapter.manga.value!; + + // Declared as a getter so concrete classes can override with a `final` field + // (which is more efficient since incognito status never changes mid-session). + bool get incognitoMode => isar.settings.getSync(227)!.incognitoMode!; + + Settings getIsarSetting() => isar.settings.getSync(227)!; + + String getMangaName() => getManga().name!; + String getSourceName() => getManga().source!; + String getChapterTitle() => chapter.name!; + + // --------------------------------------------------------------------------- + // Chapter / episode navigation + // --------------------------------------------------------------------------- + + (int, bool) getChapterIndex() => _chapterIndexWithOffset(0); + (int, bool) getPrevChapterIndex() => _chapterIndexWithOffset(1); + (int, bool) getNextChapterIndex() => _chapterIndexWithOffset(-1); + + /// Finds this [chapter] in either the filtered list or the raw list and + /// returns [index + offset]. The boolean indicates whether the filtered list + /// was used (true) or the full reversed list (false). + (int, bool) _chapterIndexWithOffset(int offset) { + final manga = getManga(); + + int? findIn(List list) { + for (var i = 0; i < list.length; i++) { + if (list[i].id == chapter.id) return i + offset; + } + return null; + } + + final index = findIn(manga.getFilteredChapterList()); + if (index != null) return (index, true); + + final all = manga.chapters.toList().reversed.toList(); + return (findIn(all)!, false); + } + + Chapter _chapterWithOffset(int offset) { + final idx = _chapterIndexWithOffset(offset); + return idx.$2 + ? getManga().getFilteredChapterList()[idx.$1] + : getManga().chapters.toList().reversed.toList()[idx.$1]; + } + + Chapter getPrevChapter() => _chapterWithOffset(1); + Chapter getNextChapter() => _chapterWithOffset(-1); + + int getChaptersLength(bool isInFilterList) => isInFilterList + ? getManga().getFilteredChapterList().length + : getManga().chapters.length; + + // --------------------------------------------------------------------------- + // History + // --------------------------------------------------------------------------- + + /// Writes a history entry for the current chapter/episode and bumps the + /// parent manga/anime's [lastRead] timestamp. + /// + /// [elapsedSeconds] accumulates watch/reading time; pass 0 to skip that + /// field (the caller is responsible for tracking wall-clock deltas). + void setHistoryUpdate({int elapsedSeconds = 0}) { + if (incognitoMode) return; + final manga = getManga(); + + isar.writeTxnSync(() { + final m = chapter.manga.value!; + m.lastRead = DateTime.now().millisecondsSinceEpoch; + m.updatedAt = DateTime.now().millisecondsSinceEpoch; + isar.mangas.putSync(m); + }); + + final isEmpty = isar.historys + .filter() + .mangaIdEqualTo(manga.id) + .isEmptySync(); + + final History history; + if (isEmpty) { + history = History( + mangaId: manga.id, + date: DateTime.now().millisecondsSinceEpoch.toString(), + itemType: manga.itemType, + chapterId: chapter.id, + )..chapter.value = chapter; + } else { + history = isar.historys.filter().mangaIdEqualTo(manga.id).findFirstSync()! + ..chapterId = chapter.id + ..chapter.value = chapter + ..date = DateTime.now().millisecondsSinceEpoch.toString(); + } + + isar.writeTxnSync(() { + history.updatedAt = DateTime.now().millisecondsSinceEpoch; + if (elapsedSeconds > 0) { + history.readingTimeSeconds = + (history.readingTimeSeconds ?? 0) + elapsedSeconds; + } + isar.historys.putSync(history); + history.chapter.saveSync(); + }); + } +} diff --git a/lib/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart b/lib/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart new file mode 100644 index 00000000..82946f93 --- /dev/null +++ b/lib/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart @@ -0,0 +1,108 @@ +import 'package:flutter/foundation.dart'; +import 'package:mangayomi/main.dart'; +import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/settings.dart'; +import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart'; + +/// Shared reader-specific settings and actions for a [Chapter]. +/// +/// This mixin builds on top of [ChapterControllerMixin] and provides: +/// - bookmark toggling +/// - auto-scroll preferences +/// +/// It is intended for reader-like controllers (manga/novel), not anime. +/// +/// Classes using this mixin may override [onSettingsMutated] to react to +/// settings changes (e.g. invalidate caches). +mixin ChapterReaderSettingsMixin on ChapterControllerMixin { + // --------------------------------------------------------------------------- + // Hooks + // --------------------------------------------------------------------------- + + /// Called after any settings mutation (e.g. [setAutoScroll], [setReaderMode], + /// [setPageMode], [setShowPageNumber], [setPageIndex]). + /// + /// Default is a no-op. Controllers can override this to invalidate caches + /// or trigger updates when settings change. + @protected + void onSettingsMutated() {} + + // --------------------------------------------------------------------------- + // Bookmarks + // --------------------------------------------------------------------------- + + /// Toggles the bookmark state of the current [chapter]. + /// + /// Updates the persisted chapter and bumps its [updatedAt] timestamp. + /// No-op in incognito mode. + void setChapterBookmarked() { + if (incognitoMode) return; + final isBookmarked = getChapterBookmarked(); + final chap = chapter; + isar.writeTxnSync(() { + chap.isBookmarked = !isBookmarked; + chap.updatedAt = DateTime.now().millisecondsSinceEpoch; + isar.chapters.putSync(chap); + }); + } + + /// Returns whether the current [chapter] is bookmarked. + /// + /// Reads directly from the database to ensure consistency. + bool getChapterBookmarked() { + return isar.chapters.getSync(chapter.id!)!.isBookmarked!; + } + + // --------------------------------------------------------------------------- + // Auto-scroll + // --------------------------------------------------------------------------- + + /// Returns the auto-scroll configuration for the current manga. + /// + /// The tuple contains: + /// - whether auto-scroll is enabled + /// - the page offset (scroll speed / distance) + /// + /// Falls back to `(false, 10)` if no custom setting exists. + (bool, double) autoScrollValues() { + final autoScrollPagesList = getIsarSetting().autoScrollPages ?? []; + final autoScrollPages = autoScrollPagesList.where( + (element) => element.mangaId == getManga().id, + ); + if (autoScrollPages.isNotEmpty) { + return ( + autoScrollPages.first.autoScroll ?? false, + autoScrollPages.first.pageOffset ?? 10, + ); + } + return (false, 10); + } + + /// Persists auto-scroll settings for the current manga. + /// + /// Replaces any existing entry for this manga with the new values and updates + /// the global settings object. Calls [onSettingsMutated] afterwards so + /// controllers can react (e.g. invalidate cached settings). + void setAutoScroll(bool value, double offset) { + List? autoScrollPagesList = []; + for (var autoScrollPages in getIsarSetting().autoScrollPages ?? []) { + if (autoScrollPages.mangaId != getManga().id) { + autoScrollPagesList.add(autoScrollPages); + } + } + autoScrollPagesList.add( + AutoScrollPages() + ..mangaId = getManga().id + ..pageOffset = offset + ..autoScroll = value, + ); + isar.writeTxnSync( + () => isar.settings.putSync( + getIsarSetting() + ..autoScrollPages = autoScrollPagesList + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + onSettingsMutated(); + } +} diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index 1869c9e8..607344fc 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -5,7 +5,8 @@ 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/manga/reader/mixins/chapter_reader_settings_mixin.dart'; +import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/track.dart'; @@ -47,7 +48,8 @@ BoxFit getBoxFit(ScaleType scaleType) { } @riverpod -class ReaderController extends _$ReaderController { +class ReaderController extends _$ReaderController + with ChapterControllerMixin, ChapterReaderSettingsMixin { @override KeepAliveLink build({required Chapter chapter}) { _keepAliveLink = ref.keepAlive(); @@ -55,20 +57,24 @@ class ReaderController extends _$ReaderController { } KeepAliveLink? _keepAliveLink; - KeepAliveLink? get keepAliveLink => _keepAliveLink; - Manga getManga() { - return chapter.manga.value!; - } + // Keep incognitoMode as a final field so it is read from Isar only once. + @override + final bool incognitoMode = isar.settings.getSync(227)!.incognitoMode!; - Chapter geChapter() { - return chapter; - } - - final incognitoMode = isar.settings.getSync(227)!.incognitoMode!; + // Override getIsarSetting to add per-instance caching; callers that mutate + // settings must call _invalidateSettingsCache() afterwards. Settings? _cachedSettings; - void _invalidateSettingsCache() => _cachedSettings = null; + @override + void onSettingsMutated() => _cachedSettings = null; + + @override + Settings getIsarSetting() => _cachedSettings ??= isar.settings.getSync(227)!; + + // --------------------------------------------------------------------------- + // Reader-specific settings + // --------------------------------------------------------------------------- ReaderMode getReaderMode() { final personalReaderModeList = @@ -79,44 +85,7 @@ class ReaderController extends _$ReaderController { if (personalReaderMode.isNotEmpty) { return personalReaderMode.first.readerMode; } - return isar.settings.getSync(227)!.defaultReaderMode; - } - - (bool, double) autoScrollValues() { - final autoScrollPagesList = getIsarSetting().autoScrollPages ?? []; - final autoScrollPages = autoScrollPagesList.where( - (element) => element.mangaId == getManga().id, - ); - if (autoScrollPages.isNotEmpty) { - return ( - autoScrollPages.first.autoScroll ?? false, - autoScrollPages.first.pageOffset ?? 10, - ); - } - return (false, 10); - } - - void setAutoScroll(bool value, double offset) { - List? autoScrollPagesList = []; - for (var autoScrollPages in getIsarSetting().autoScrollPages ?? []) { - if (autoScrollPages.mangaId != getManga().id) { - autoScrollPagesList.add(autoScrollPages); - } - } - autoScrollPagesList.add( - AutoScrollPages() - ..mangaId = getManga().id - ..pageOffset = offset - ..autoScroll = value, - ); - isar.writeTxnSync( - () => isar.settings.putSync( - getIsarSetting() - ..autoScrollPages = autoScrollPagesList - ..updatedAt = DateTime.now().millisecondsSinceEpoch, - ), - ); - _invalidateSettingsCache(); + return getIsarSetting().defaultReaderMode; } PageMode getPageMode() { @@ -150,7 +119,7 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); - _invalidateSettingsCache(); + onSettingsMutated(); } void setPageMode(PageMode newPageMode) { @@ -172,7 +141,7 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); - _invalidateSettingsCache(); + onSettingsMutated(); } void setShowPageNumber(bool value) { @@ -184,154 +153,18 @@ class ReaderController extends _$ReaderController { ..updatedAt = DateTime.now().millisecondsSinceEpoch, ), ); - _invalidateSettingsCache(); + onSettingsMutated(); } } - Settings getIsarSetting() => _cachedSettings ??= isar.settings.getSync(227)!; - bool getShowPageNumber() { if (!incognitoMode) return getIsarSetting().showPagesNumber!; return true; } - void setMangaHistoryUpdate({int readingTimeSeconds = 0}) { - if (incognitoMode) return; - isar.writeTxnSync(() { - Manga? manga = chapter.manga.value; - manga!.lastRead = DateTime.now().millisecondsSinceEpoch; - manga.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.mangas.putSync(manga); - }); - History? history; - - final empty = isar.historys - .filter() - .mangaIdEqualTo(getManga().id) - .isEmptySync(); - - if (empty) { - history = History( - mangaId: getManga().id, - date: DateTime.now().millisecondsSinceEpoch.toString(), - itemType: getManga().itemType, - chapterId: chapter.id, - )..chapter.value = chapter; - } else { - history = - (isar.historys - .filter() - .mangaIdEqualTo(getManga().id) - .findFirstSync())! - ..chapterId = chapter.id - ..chapter.value = chapter - ..date = DateTime.now().millisecondsSinceEpoch.toString(); - } - isar.writeTxnSync(() { - history!.updatedAt = DateTime.now().millisecondsSinceEpoch; - if (readingTimeSeconds > 0) { - history.readingTimeSeconds = - (history.readingTimeSeconds ?? 0) + readingTimeSeconds; - } - isar.historys.putSync(history); - history.chapter.saveSync(); - }); - } - - void setChapterBookmarked() { - if (incognitoMode) return; - final isBookmarked = getChapterBookmarked(); - final chap = chapter; - isar.writeTxnSync(() { - chap.isBookmarked = !isBookmarked; - chap.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.chapters.putSync(chap); - }); - } - - bool getChapterBookmarked() { - return isar.chapters.getSync(chapter.id!)!.isBookmarked!; - } - - (int, bool) getPrevChapterIndex() { - final chapters = getManga().getFilteredChapterList(); - int? index; - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i + 1; - } - } - if (index == null) { - final chapters = getManga().chapters.toList().reversed.toList(); - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i + 1; - } - } - return (index!, false); - } - return (index, true); - } - - (int, bool) getNextChapterIndex() { - final chapters = getManga().getFilteredChapterList(); - int? index; - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i - 1; - } - } - if (index == null) { - final chapters = getManga().chapters.toList().reversed.toList(); - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i - 1; - } - } - return (index!, false); - } - return (index, true); - } - - (int, bool) getChapterIndex() { - final chapters = getManga().getFilteredChapterList(); - int? index; - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i; - } - } - if (index == null) { - final chapters = getManga().chapters.toList().reversed.toList(); - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i; - } - } - return (index!, false); - } - return (index, true); - } - - Chapter getPrevChapter() { - final prevChapIdx = getPrevChapterIndex(); - return prevChapIdx.$2 - ? getManga().getFilteredChapterList()[prevChapIdx.$1] - : getManga().chapters.toList().reversed.toList()[prevChapIdx.$1]; - } - - Chapter getNextChapter() { - final nextChapIdx = getNextChapterIndex(); - return nextChapIdx.$2 - ? getManga().getFilteredChapterList()[nextChapIdx.$1] - : getManga().chapters.toList().reversed.toList()[nextChapIdx.$1]; - } - - int getChaptersLength(bool isInFilterList) { - return isInFilterList - ? getManga().getFilteredChapterList().length - : getManga().chapters.length; - } + // --------------------------------------------------------------------------- + // Page tracking + // --------------------------------------------------------------------------- int getPageIndex() { if (incognitoMode) return 0; @@ -392,7 +225,7 @@ class ReaderController extends _$ReaderController { chap.updatedAt = DateTime.now().millisecondsSinceEpoch; isar.chapters.putSync(chap); }); - _invalidateSettingsCache(); + onSettingsMutated(); if (isRead) { chapter.updateTrackChapterRead(ref); if (ref.read(deleteDownloadAfterReadingStateProvider)) { @@ -401,24 +234,12 @@ class ReaderController extends _$ReaderController { } } } - - String getMangaName() { - return getManga().name!; - } - - String getSourceName() { - return getManga().source!; - } - - String getChapterTitle() { - return chapter.name!; - } } extension ChapterExtensions on Chapter { void updateTrackChapterRead(dynamic ref) { if (!(ref is WidgetRef || ref is Ref)) return; - final updateProgressAfterReading = ref.watch( + final updateProgressAfterReading = ref.read( updateProgressAfterReadingStateProvider, ); if (!updateProgressAfterReading) return; diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 76ee6588..986765b4 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -166,8 +166,8 @@ class _MangaChapterPageGalleryState void dispose() { WidgetsBinding.instance.removeObserver(this); _readingStopwatch.stop(); - _readerController.setMangaHistoryUpdate( - readingTimeSeconds: _readingStopwatch.elapsed.inSeconds, + _readerController.setHistoryUpdate( + elapsedSeconds: _readingStopwatch.elapsed.inSeconds, ); _rebuildDetail.close(); _doubleClickAnimationController.dispose(); @@ -1170,7 +1170,7 @@ class _MangaChapterPageGalleryState // proactively start loading adjacent chapters in background _proactivePreload(); - _readerController.setMangaHistoryUpdate(); + _readerController.setHistoryUpdate(); // Use post-frame callback instead of Future.delayed(1ms) timing hack await Future(() {}); final fullScreenReader = ref.watch(fullScreenReaderStateProvider); diff --git a/lib/modules/novel/novel_reader_controller_provider.dart b/lib/modules/novel/novel_reader_controller_provider.dart index c390f401..daae6821 100644 --- a/lib/modules/novel/novel_reader_controller_provider.dart +++ b/lib/modules/novel/novel_reader_controller_provider.dart @@ -1,110 +1,33 @@ -import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/models/history.dart'; -import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart'; +import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'novel_reader_controller_provider.g.dart'; @riverpod -class NovelReaderController extends _$NovelReaderController { +class NovelReaderController extends _$NovelReaderController + with ChapterControllerMixin, ChapterReaderSettingsMixin { @override void build({required Chapter chapter}) {} - Manga getManga() { - return chapter.manga.value!; - } + // Keep incognitoMode as a final field (read once, not on every access). + @override + final bool incognitoMode = isar.settings.getSync(227)!.incognitoMode!; - Chapter geChapter() { - return chapter; - } + // Override getIsarSetting to add per-instance caching; callers that mutate + // settings must call _invalidateSettingsCache() afterwards. + Settings? _cachedSettings; + @override + void onSettingsMutated() => _cachedSettings = null; - final incognitoMode = isar.settings.getSync(227)!.incognitoMode!; + @override + Settings getIsarSetting() => _cachedSettings ??= isar.settings.getSync(227)!; - Settings getIsarSetting() { - return isar.settings.getSync(227)!; - } - - (bool, double) autoScrollValues() { - final autoScrollPagesList = getIsarSetting().autoScrollPages ?? []; - final autoScrollPages = autoScrollPagesList.where( - (element) => element.mangaId == getManga().id, - ); - if (autoScrollPages.isNotEmpty) { - return ( - autoScrollPages.first.autoScroll ?? false, - autoScrollPages.first.pageOffset ?? 10, - ); - } - return (false, 10); - } - - void setAutoScroll(bool value, double offset) { - List? autoScrollPagesList = []; - for (var autoScrollPages in getIsarSetting().autoScrollPages ?? []) { - if (autoScrollPages.mangaId != getManga().id) { - autoScrollPagesList.add(autoScrollPages); - } - } - autoScrollPagesList.add( - AutoScrollPages() - ..mangaId = getManga().id - ..pageOffset = offset - ..autoScroll = value, - ); - isar.writeTxnSync( - () => isar.settings.putSync( - getIsarSetting() - ..autoScrollPages = autoScrollPagesList - ..updatedAt = DateTime.now().millisecondsSinceEpoch, - ), - ); - } - - void setMangaHistoryUpdate({int readingTimeSeconds = 0}) { - if (incognitoMode) return; - isar.writeTxnSync(() { - Manga? manga = chapter.manga.value; - manga!.lastRead = DateTime.now().millisecondsSinceEpoch; - manga.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.mangas.putSync(manga); - }); - History? history; - - final empty = isar.historys - .filter() - .mangaIdEqualTo(getManga().id) - .isEmptySync(); - - if (empty) { - history = History( - mangaId: getManga().id, - date: DateTime.now().millisecondsSinceEpoch.toString(), - itemType: getManga().itemType, - chapterId: chapter.id, - )..chapter.value = chapter; - } else { - history = - (isar.historys - .filter() - .mangaIdEqualTo(getManga().id) - .findFirstSync())! - ..chapterId = chapter.id - ..chapter.value = chapter - ..date = DateTime.now().millisecondsSinceEpoch.toString(); - } - isar.writeTxnSync(() { - history!.updatedAt = DateTime.now().millisecondsSinceEpoch; - if (readingTimeSeconds > 0) { - history.readingTimeSeconds = - (history.readingTimeSeconds ?? 0) + readingTimeSeconds; - } - isar.historys.putSync(history); - history.chapter.saveSync(); - }); - } + // --------------------------------------------------------------------------- + // Scroll-position tracking + // --------------------------------------------------------------------------- void setChapterOffset(double newOffset, double maxOffset, bool save) { if (incognitoMode) return; @@ -120,111 +43,4 @@ class NovelReaderController extends _$NovelReaderController { }); } } - - void setChapterBookmarked() { - if (incognitoMode) return; - final isBookmarked = getChapterBookmarked(); - final chap = chapter; - isar.writeTxnSync(() { - chap.isBookmarked = !isBookmarked; - chap.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.chapters.putSync(chap); - }); - } - - bool getChapterBookmarked() { - return isar.chapters.getSync(chapter.id!)!.isBookmarked!; - } - - (int, bool) getPrevChapterIndex() { - final chapters = getManga().getFilteredChapterList(); - int? index; - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i + 1; - } - } - if (index == null) { - final chapters = getManga().chapters.toList().toList(); - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i + 1; - } - } - return (index!, false); - } - return (index, true); - } - - (int, bool) getNextChapterIndex() { - final chapters = getManga().getFilteredChapterList(); - int? index; - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i - 1; - } - } - if (index == null) { - final chapters = getManga().chapters.toList().toList(); - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i - 1; - } - } - return (index!, false); - } - return (index, true); - } - - (int, bool) getChapterIndex() { - final chapters = getManga().getFilteredChapterList(); - int? index; - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i; - } - } - if (index == null) { - final chapters = getManga().chapters.toList().toList(); - for (var i = 0; i < chapters.length; i++) { - if (chapters[i].id == chapter.id) { - index = i; - } - } - return (index!, false); - } - return (index, true); - } - - Chapter getPrevChapter() { - final prevChapIdx = getPrevChapterIndex(); - return prevChapIdx.$2 - ? getManga().getFilteredChapterList()[prevChapIdx.$1] - : getManga().chapters.toList().toList()[prevChapIdx.$1]; - } - - Chapter getNextChapter() { - final nextChapIdx = getNextChapterIndex(); - return nextChapIdx.$2 - ? getManga().getFilteredChapterList()[nextChapIdx.$1] - : getManga().chapters.toList().toList()[nextChapIdx.$1]; - } - - int getChaptersLength(bool isInFilterList) { - return isInFilterList - ? getManga().getFilteredChapterList().length - : getManga().chapters.length; - } - - String getMangaName() { - return getManga().name!; - } - - String getSourceName() { - return getManga().source!; - } - - String getChapterTitle() { - return chapter.name!; - } } diff --git a/lib/modules/novel/novel_reader_view.dart b/lib/modules/novel/novel_reader_view.dart index 261e9cb8..95081270 100644 --- a/lib/modules/novel/novel_reader_view.dart +++ b/lib/modules/novel/novel_reader_view.dart @@ -90,8 +90,8 @@ class _NovelWebViewState extends ConsumerState _readingStopwatch.stop(); WidgetsBinding.instance.removeObserver(this); _readerController.setChapterOffset(offset, maxOffset, true); - _readerController.setMangaHistoryUpdate( - readingTimeSeconds: _readingStopwatch.elapsed.inSeconds, + _readerController.setHistoryUpdate( + elapsedSeconds: _readingStopwatch.elapsed.inSeconds, ); _scrollController.removeListener(onScroll); _scrollController.dispose(); From 09a4517d331298b5ce89cc6767091fb475b3a00b Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:38:37 +0200 Subject: [PATCH 05/20] Fix reader --- .../anime_player_controller_provider.dart | 2 - .../mixins/chapter_controller_mixin.dart | 27 ++-- .../providers/reader_controller_provider.dart | 115 ++++++++++-------- lib/utils/extensions/chapter.dart | 4 +- 4 files changed, 78 insertions(+), 70 deletions(-) diff --git a/lib/modules/anime/providers/anime_player_controller_provider.dart b/lib/modules/anime/providers/anime_player_controller_provider.dart index 6af26c78..184668a4 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.dart @@ -40,8 +40,6 @@ class AnimeStreamController extends _$AnimeStreamController // --------------------------------------------------------------------------- (int, bool) getEpisodeIndex() => getChapterIndex(); - (int, bool) getPrevEpisodeIndex() => getPrevChapterIndex(); - (int, bool) getNextEpisodeIndex() => getNextChapterIndex(); Chapter getPrevEpisode() => getPrevChapter(); Chapter getNextEpisode() => getNextChapter(); diff --git a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart index d53c329f..a3ac2a8d 100644 --- a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart +++ b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart @@ -44,12 +44,12 @@ mixin ChapterControllerMixin { // --------------------------------------------------------------------------- (int, bool) getChapterIndex() => _chapterIndexWithOffset(0); - (int, bool) getPrevChapterIndex() => _chapterIndexWithOffset(1); - (int, bool) getNextChapterIndex() => _chapterIndexWithOffset(-1); + Chapter getPrevChapter() => _chapterWithOffset(-1); + Chapter getNextChapter() => _chapterWithOffset(1); /// Finds this [chapter] in either the filtered list or the raw list and /// returns [index + offset]. The boolean indicates whether the filtered list - /// was used (true) or the full reversed list (false). + /// was used (true) or the full list (false). (int, bool) _chapterIndexWithOffset(int offset) { final manga = getManga(); @@ -60,25 +60,26 @@ mixin ChapterControllerMixin { return null; } - final index = findIn(manga.getFilteredChapterList()); + final index = findIn(manga.getChapterListForReading()); if (index != null) return (index, true); - - final all = manga.chapters.toList().reversed.toList(); + // Fallback to raw list if chapter was filtered out. + final all = manga.chapters.toList(); return (findIn(all)!, false); } Chapter _chapterWithOffset(int offset) { final idx = _chapterIndexWithOffset(offset); - return idx.$2 - ? getManga().getFilteredChapterList()[idx.$1] - : getManga().chapters.toList().reversed.toList()[idx.$1]; + final list = idx.$2 + ? getManga().getChapterListForReading() + : getManga().chapters.toList(); + if (idx.$1 < 0 || idx.$1 >= list.length) { + throw RangeError('No chapter at offset $offset from ${chapter.id}'); + } + return list[idx.$1]; } - Chapter getPrevChapter() => _chapterWithOffset(1); - Chapter getNextChapter() => _chapterWithOffset(-1); - int getChaptersLength(bool isInFilterList) => isInFilterList - ? getManga().getFilteredChapterList().length + ? getManga().getChapterListForReading().length : getManga().chapters.length; // --------------------------------------------------------------------------- diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index 607344fc..d521d59a 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -294,40 +294,37 @@ extension ChapterExtensions on Chapter { } extension MangaExtensions on Manga { - List getFilteredChapterList() { - final data = this.chapters.toList().reversed.toList(); + // ── For the READER: always ascending story order, filters applied ────────── + List getChapterListForReading() { final settings = isar.settings.getSync(227)!; + final filterUnread = (settings.chapterFilterUnreadList! - .where((element) => element.mangaId == id) - .toList() + .where((e) => e.mangaId == id) .firstOrNull ?? ChapterFilterUnread(mangaId: id, type: 0)) .type!; final filterBookmarked = (settings.chapterFilterBookmarkedList! - .where((element) => element.mangaId == id) - .toList() + .where((e) => e.mangaId == id) .firstOrNull ?? ChapterFilterBookmarked(mangaId: id, type: 0)) .type!; + final filterDownloaded = (settings.chapterFilterDownloadedList! - .where((element) => element.mangaId == id) - .toList() + .where((e) => e.mangaId == id) .firstOrNull ?? ChapterFilterDownloaded(mangaId: id, type: 0)) .type!; - final sortChapter = - (settings.sortChapterList! - .where((element) => element.mangaId == id) - .toList() - .firstOrNull ?? - SortChapter(mangaId: id, index: 1, reverse: false)) - .index; final filterScanlator = _getFilterScanlator(this) ?? []; + + // Canonical ascending order (ch1 ... chN) — reader always moves forward. + final data = chapters + .toList(); // keep DB/insertion order, assumed ascending + final chapterIds = data.map((c) => c.id).whereType().toList(); final downloadedIds = (filterDownloaded == 0 || chapterIds.isEmpty) ? const {} @@ -338,56 +335,68 @@ extension MangaExtensions on Manga { .findAllSync() .map((d) => d.id!) .toSet(); - List? chapterList; - chapterList = data + + return data .where( - (element) => filterUnread == 1 - ? element.isRead == false + (e) => filterUnread == 1 + ? e.isRead == false : filterUnread == 2 - ? element.isRead == true + ? e.isRead == true : true, ) .where( - (element) => filterBookmarked == 1 - ? element.isBookmarked == true + (e) => filterBookmarked == 1 + ? e.isBookmarked == true : filterBookmarked == 2 - ? element.isBookmarked == false + ? e.isBookmarked == false : true, ) - .where((element) { + .where((e) { if (filterDownloaded == 0) return true; - final isDownloaded = downloadedIds.contains(element.id); - return filterDownloaded == 1 ? isDownloaded : !isDownloaded; + final dl = downloadedIds.contains(e.id); + return filterDownloaded == 1 ? dl : !dl; }) - .where((element) => !filterScanlator.contains(element.scanlator)) + .where((e) => !filterScanlator.contains(e.scanlator)) .toList(); - List chapters = sortChapter == 0 - ? chapterList.reversed.toList() - : chapterList; - if (sortChapter == 0) { - chapters.sort((a, b) { - return (a.scanlator == null || - b.scanlator == null || - a.dateUpload == null || - b.dateUpload == null) - ? 0 - : a.scanlator!.compareTo(b.scanlator!) | - a.dateUpload!.compareTo(b.dateUpload!); - }); - } else if (sortChapter == 2) { - chapters.sort((a, b) { - return (a.dateUpload == null || b.dateUpload == null) - ? 0 - : int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); - }); - } else if (sortChapter == 3) { - chapters.sort((a, b) { - return (a.name == null || b.name == null) - ? 0 - : a.name!.compareTo(b.name!); - }); + } + + // ── For the UI LIST: filters + user-chosen sort + reverse ───────────────── + List getFilteredChapterList() { + final settings = isar.settings.getSync(227)!; + + final sortChapterEntry = + settings.sortChapterList!.where((e) => e.mangaId == id).firstOrNull ?? + SortChapter(mangaId: id, index: 1, reverse: false); + final sortIndex = sortChapterEntry.index!; + final reverse = sortChapterEntry.reverse!; + + // Start from the reading list so filter logic lives in one place. + List list = getChapterListForReading(); + + switch (sortIndex) { + case 0: // by scanlator, then date + list.sort((a, b) { + if (a.scanlator == null || b.scanlator == null) return 0; + final s = a.scanlator!.compareTo(b.scanlator!); + if (s != 0) return s; + if (a.dateUpload == null || b.dateUpload == null) return 0; + return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); + }); + case 1: // by chapter number - reading list is already ascending + break; + case 2: // by upload date + list.sort((a, b) { + if (a.dateUpload == null || b.dateUpload == null) return 0; + return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); + }); + case 3: // by name + list.sort((a, b) { + if (a.name == null || b.name == null) return 0; + return a.name!.compareTo(b.name!); + }); } - return chapters; + + return reverse ? list.reversed.toList() : list; } } diff --git a/lib/utils/extensions/chapter.dart b/lib/utils/extensions/chapter.dart index a2081a88..bbdea09f 100644 --- a/lib/utils/extensions/chapter.dart +++ b/lib/utils/extensions/chapter.dart @@ -19,9 +19,9 @@ extension ChapterExtension on Chapter { if (ignoreIsRead || !isRead!) { await pushMangaReaderView(context: context, chapter: this); } else { - final filteredChaps = manga.value!.getFilteredChapterList(); + final filteredChaps = manga.value!.getChapterListForReading(); bool exist = false; - for (var filteredChap in filteredChaps.reversed) { + for (var filteredChap in filteredChaps) { if (filteredChap.toJson().toString() == toJson().toString()) { exist = true; } From 6aef999fd11c014b08e2e56de25a8745cbece9a6 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:41:19 +0200 Subject: [PATCH 06/20] move extensions to correct files --- .../anime_player_controller_provider.dart | 2 +- .../providers/library_state_provider.dart | 2 +- .../manga/detail/manga_detail_view.dart | 2 +- .../mixins/chapter_controller_mixin.dart | 2 +- .../providers/reader_controller_provider.dart | 181 ------------------ .../widgets/btn_chapter_list_dialog.dart | 2 +- lib/utils/extensions/chapter.dart | 65 ++++++- lib/utils/extensions/manga.dart | 115 +++++++++++ 8 files changed, 184 insertions(+), 187 deletions(-) create mode 100644 lib/utils/extensions/manga.dart diff --git a/lib/modules/anime/providers/anime_player_controller_provider.dart b/lib/modules/anime/providers/anime_player_controller_provider.dart index 184668a4..23bdc76f 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.dart @@ -5,7 +5,7 @@ import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/track.dart'; import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart'; import 'package:mangayomi/services/aniskip.dart'; import 'package:mangayomi/utils/chapter_recognition.dart'; diff --git a/lib/modules/library/providers/library_state_provider.dart b/lib/modules/library/providers/library_state_provider.dart index 8f739452..7c5c64dd 100644 --- a/lib/modules/library/providers/library_state_provider.dart +++ b/lib/modules/library/providers/library_state_provider.dart @@ -4,7 +4,7 @@ import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'library_state_provider.g.dart'; diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 93df9b56..5bc42e45 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -22,7 +22,7 @@ import 'package:mangayomi/modules/library/providers/local_archive.dart'; import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart'; import 'package:mangayomi/modules/manga/detail/widgets/tracker_search_widget.dart'; import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/modules/more/providers/algorithm_weights_state_provider.dart'; import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart'; import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart'; diff --git a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart index a3ac2a8d..4080136a 100644 --- a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart +++ b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart @@ -4,7 +4,7 @@ import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/utils/extensions/manga.dart'; /// Shared navigation and history logic used by [ReaderController], /// [NovelReaderController], and [AnimeStreamController]. diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.dart b/lib/modules/manga/reader/providers/reader_controller_provider.dart index d521d59a..01900c29 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.dart @@ -1,21 +1,12 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/misc.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/modules/manga/reader/mixins/chapter_reader_settings_mixin.dart'; import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart'; -import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; -import 'package:mangayomi/models/track.dart'; -import 'package:mangayomi/models/track_preference.dart'; -import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart'; import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart'; import 'package:mangayomi/modules/more/settings/downloads/providers/downloads_state_provider.dart'; -import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart'; -import 'package:mangayomi/utils/chapter_recognition.dart'; import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'reader_controller_provider.g.dart'; @@ -235,175 +226,3 @@ class ReaderController extends _$ReaderController } } } - -extension ChapterExtensions on Chapter { - void updateTrackChapterRead(dynamic ref) { - if (!(ref is WidgetRef || ref is Ref)) return; - final updateProgressAfterReading = ref.read( - updateProgressAfterReadingStateProvider, - ); - if (!updateProgressAfterReading) return; - final manga = this.manga.value!; - final chapterNumber = ChapterRecognition().parseChapterNumber( - manga.name!, - name!, - ); - - final tracks = isar.tracks - .filter() - .idIsNotNull() - .itemTypeEqualTo(manga.itemType) - .mangaIdEqualTo(manga.id!) - .findAllSync(); - - if (tracks.isEmpty) return; - for (var track in tracks) { - final service = isar.trackPreferences - .filter() - .syncIdIsNotNull() - .syncIdEqualTo(track.syncId) - .findFirstSync(); - if (!(service == null || chapterNumber <= (track.lastChapterRead ?? 0))) { - if (track.status != TrackStatus.completed) { - track.lastChapterRead = chapterNumber; - if (track.lastChapterRead == track.totalChapter && - (track.totalChapter ?? 0) > 0) { - track.status = TrackStatus.completed; - track.finishedReadingDate = DateTime.now().millisecondsSinceEpoch; - } else { - track.status = manga.itemType == ItemType.manga - ? TrackStatus.reading - : TrackStatus.watching; - if (track.lastChapterRead == 1) { - track.startedReadingDate = DateTime.now().millisecondsSinceEpoch; - } - } - } - ref - .read( - trackStateProvider( - track: track, - itemType: manga.itemType, - widgetRef: ref, - ).notifier, - ) - .updateManga(); - } - } - } -} - -extension MangaExtensions on Manga { - // ── For the READER: always ascending story order, filters applied ────────── - List getChapterListForReading() { - final settings = isar.settings.getSync(227)!; - - final filterUnread = - (settings.chapterFilterUnreadList! - .where((e) => e.mangaId == id) - .firstOrNull ?? - ChapterFilterUnread(mangaId: id, type: 0)) - .type!; - - final filterBookmarked = - (settings.chapterFilterBookmarkedList! - .where((e) => e.mangaId == id) - .firstOrNull ?? - ChapterFilterBookmarked(mangaId: id, type: 0)) - .type!; - - final filterDownloaded = - (settings.chapterFilterDownloadedList! - .where((e) => e.mangaId == id) - .firstOrNull ?? - ChapterFilterDownloaded(mangaId: id, type: 0)) - .type!; - - final filterScanlator = _getFilterScanlator(this) ?? []; - - // Canonical ascending order (ch1 ... chN) — reader always moves forward. - final data = chapters - .toList(); // keep DB/insertion order, assumed ascending - - 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(); - - return data - .where( - (e) => filterUnread == 1 - ? e.isRead == false - : filterUnread == 2 - ? e.isRead == true - : true, - ) - .where( - (e) => filterBookmarked == 1 - ? e.isBookmarked == true - : filterBookmarked == 2 - ? e.isBookmarked == false - : true, - ) - .where((e) { - if (filterDownloaded == 0) return true; - final dl = downloadedIds.contains(e.id); - return filterDownloaded == 1 ? dl : !dl; - }) - .where((e) => !filterScanlator.contains(e.scanlator)) - .toList(); - } - - // ── For the UI LIST: filters + user-chosen sort + reverse ───────────────── - List getFilteredChapterList() { - final settings = isar.settings.getSync(227)!; - - final sortChapterEntry = - settings.sortChapterList!.where((e) => e.mangaId == id).firstOrNull ?? - SortChapter(mangaId: id, index: 1, reverse: false); - final sortIndex = sortChapterEntry.index!; - final reverse = sortChapterEntry.reverse!; - - // Start from the reading list so filter logic lives in one place. - List list = getChapterListForReading(); - - switch (sortIndex) { - case 0: // by scanlator, then date - list.sort((a, b) { - if (a.scanlator == null || b.scanlator == null) return 0; - final s = a.scanlator!.compareTo(b.scanlator!); - if (s != 0) return s; - if (a.dateUpload == null || b.dateUpload == null) return 0; - return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); - }); - case 1: // by chapter number - reading list is already ascending - break; - case 2: // by upload date - list.sort((a, b) { - if (a.dateUpload == null || b.dateUpload == null) return 0; - return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); - }); - case 3: // by name - list.sort((a, b) { - if (a.name == null || b.name == null) return 0; - return a.name!.compareTo(b.name!); - }); - } - - return reverse ? list.reversed.toList() : list; - } -} - -List? _getFilterScanlator(Manga manga) { - final scanlators = isar.settings.getSync(227)!.filterScanlatorList ?? []; - final filter = scanlators - .where((element) => element.mangaId == manga.id) - .toList(); - return filter.firstOrNull?.scanlators; -} diff --git a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart index 7b2661a7..07236d9b 100644 --- a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart +++ b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart @@ -4,7 +4,7 @@ import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/utils/extensions/manga.dart'; import 'package:mangayomi/modules/manga/reader/reader_view.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/date.dart'; diff --git a/lib/utils/extensions/chapter.dart b/lib/utils/extensions/chapter.dart index bbdea09f..f469f97c 100644 --- a/lib/utils/extensions/chapter.dart +++ b/lib/utils/extensions/chapter.dart @@ -1,13 +1,21 @@ import 'dart:io'; 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/manga.dart'; +import 'package:mangayomi/models/track.dart'; +import 'package:mangayomi/models/track_preference.dart'; +import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/utils/extensions/manga.dart'; +import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart'; import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/services/download_manager/download_isolate_pool.dart'; import 'package:mangayomi/services/download_manager/m_downloader.dart'; +import 'package:mangayomi/utils/chapter_recognition.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:path/path.dart' as p; @@ -80,4 +88,59 @@ extension ChapterExtension on Chapter { cancelDownloads(download.id); } + + void updateTrackChapterRead(dynamic ref) { + if (!(ref is WidgetRef || ref is Ref)) return; + final updateProgressAfterReading = ref.read( + updateProgressAfterReadingStateProvider, + ); + if (!updateProgressAfterReading) return; + final manga = this.manga.value!; + final chapterNumber = ChapterRecognition().parseChapterNumber( + manga.name!, + name!, + ); + + final tracks = isar.tracks + .filter() + .idIsNotNull() + .itemTypeEqualTo(manga.itemType) + .mangaIdEqualTo(manga.id!) + .findAllSync(); + + if (tracks.isEmpty) return; + for (var track in tracks) { + final service = isar.trackPreferences + .filter() + .syncIdIsNotNull() + .syncIdEqualTo(track.syncId) + .findFirstSync(); + if (!(service == null || chapterNumber <= (track.lastChapterRead ?? 0))) { + if (track.status != TrackStatus.completed) { + track.lastChapterRead = chapterNumber; + if (track.lastChapterRead == track.totalChapter && + (track.totalChapter ?? 0) > 0) { + track.status = TrackStatus.completed; + track.finishedReadingDate = DateTime.now().millisecondsSinceEpoch; + } else { + track.status = manga.itemType == ItemType.manga + ? TrackStatus.reading + : TrackStatus.watching; + if (track.lastChapterRead == 1) { + track.startedReadingDate = DateTime.now().millisecondsSinceEpoch; + } + } + } + ref + .read( + trackStateProvider( + track: track, + itemType: manga.itemType, + widgetRef: ref, + ).notifier, + ) + .updateManga(); + } + } + } } diff --git a/lib/utils/extensions/manga.dart b/lib/utils/extensions/manga.dart new file mode 100644 index 00000000..2d2f92ce --- /dev/null +++ b/lib/utils/extensions/manga.dart @@ -0,0 +1,115 @@ +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/manga.dart'; +import 'package:mangayomi/models/settings.dart'; + +extension MangaExtensions on Manga { + // ── For the READER: always ascending story order, filters applied ────────── + List getChapterListForReading() { + final settings = isar.settings.getSync(227)!; + + final filterUnread = + (settings.chapterFilterUnreadList! + .where((e) => e.mangaId == id) + .firstOrNull ?? + ChapterFilterUnread(mangaId: id, type: 0)) + .type!; + + final filterBookmarked = + (settings.chapterFilterBookmarkedList! + .where((e) => e.mangaId == id) + .firstOrNull ?? + ChapterFilterBookmarked(mangaId: id, type: 0)) + .type!; + + final filterDownloaded = + (settings.chapterFilterDownloadedList! + .where((e) => e.mangaId == id) + .firstOrNull ?? + ChapterFilterDownloaded(mangaId: id, type: 0)) + .type!; + + final scanlators = settings.filterScanlatorList ?? []; + final filter = scanlators.where((e) => e.mangaId == id).toList(); + final filterScanlator = filter.firstOrNull?.scanlators ?? []; + + // Canonical ascending order (ch1 ... chN) — reader always moves forward. + final data = chapters + .toList(); // keep DB/insertion order, assumed ascending + + 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(); + + return data + .where( + (e) => filterUnread == 1 + ? e.isRead == false + : filterUnread == 2 + ? e.isRead == true + : true, + ) + .where( + (e) => filterBookmarked == 1 + ? e.isBookmarked == true + : filterBookmarked == 2 + ? e.isBookmarked == false + : true, + ) + .where((e) { + if (filterDownloaded == 0) return true; + final dl = downloadedIds.contains(e.id); + return filterDownloaded == 1 ? dl : !dl; + }) + .where((e) => !filterScanlator.contains(e.scanlator)) + .toList(); + } + + // ── For the UI LIST: filters + user-chosen sort + reverse ───────────────── + List getFilteredChapterList() { + final settings = isar.settings.getSync(227)!; + + final sortChapterEntry = + settings.sortChapterList!.where((e) => e.mangaId == id).firstOrNull ?? + SortChapter(mangaId: id, index: 1, reverse: false); + final sortIndex = sortChapterEntry.index!; + final reverse = sortChapterEntry.reverse!; + + // Start from the reading list so filter logic lives in one place. + List list = getChapterListForReading(); + + switch (sortIndex) { + case 0: // by scanlator, then date + list.sort((a, b) { + if (a.scanlator == null || b.scanlator == null) return 0; + final s = a.scanlator!.compareTo(b.scanlator!); + if (s != 0) return s; + if (a.dateUpload == null || b.dateUpload == null) return 0; + return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); + }); + case 1: // by chapter number - reading list is already ascending + break; + case 2: // by upload date + list.sort((a, b) { + if (a.dateUpload == null || b.dateUpload == null) return 0; + return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); + }); + case 3: // by name + list.sort((a, b) { + if (a.name == null || b.name == null) return 0; + return a.name!.compareTo(b.name!); + }); + } + + return reverse ? list.reversed.toList() : list; + } +} From 6ae2ac1a95063a298b05e46c83d877014e8e4cfc Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:35:43 +0200 Subject: [PATCH 07/20] Why are there two searchproviders? --- .../global_search/global_search_screen.dart | 2 +- .../manga/detail/widgets/migrate_screen.dart | 2 +- .../services/mass_migration_service.dart | 2 +- lib/services/search_.dart | 48 -------- lib/services/search_.g.dart | 109 ------------------ 5 files changed, 3 insertions(+), 160 deletions(-) delete mode 100644 lib/services/search_.dart delete mode 100644 lib/services/search_.g.dart diff --git a/lib/modules/browse/global_search/global_search_screen.dart b/lib/modules/browse/global_search/global_search_screen.dart index 8e2e00d1..e274f532 100644 --- a/lib/modules/browse/global_search/global_search_screen.dart +++ b/lib/modules/browse/global_search/global_search_screen.dart @@ -11,7 +11,7 @@ import 'package:mangayomi/modules/manga/home/manga_home_screen.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/router/router.dart'; import 'package:mangayomi/models/source.dart'; -import 'package:mangayomi/services/search_.dart'; +import 'package:mangayomi/services/search.dart'; import 'package:mangayomi/utils/cached_network.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/utils/constant.dart'; diff --git a/lib/modules/manga/detail/widgets/migrate_screen.dart b/lib/modules/manga/detail/widgets/migrate_screen.dart index 2d5a3bab..e9b1ef58 100644 --- a/lib/modules/manga/detail/widgets/migrate_screen.dart +++ b/lib/modules/manga/detail/widgets/migrate_screen.dart @@ -17,7 +17,7 @@ import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_pr import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/services/get_detail.dart'; -import 'package:mangayomi/services/search_.dart'; +import 'package:mangayomi/services/search.dart'; import 'package:mangayomi/utils/cached_network.dart'; import 'package:mangayomi/utils/date.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; diff --git a/lib/modules/mass_migration/services/mass_migration_service.dart b/lib/modules/mass_migration/services/mass_migration_service.dart index 0670c2dc..e66d926c 100644 --- a/lib/modules/mass_migration/services/mass_migration_service.dart +++ b/lib/modules/mass_migration/services/mass_migration_service.dart @@ -13,7 +13,7 @@ import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.da import 'package:mangayomi/modules/manga/detail/providers/isar_providers.dart'; import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/services/get_detail.dart'; -import 'package:mangayomi/services/search_.dart'; +import 'package:mangayomi/services/search.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart'; Future migrateLibraryItem({ diff --git a/lib/services/search_.dart b/lib/services/search_.dart deleted file mode 100644 index 7ae16a41..00000000 --- a/lib/services/search_.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:isar_community/isar.dart'; -import 'package:mangayomi/eval/model/m_manga.dart'; -import 'package:mangayomi/eval/model/m_pages.dart'; -import 'package:mangayomi/main.dart'; -import 'package:mangayomi/models/manga.dart'; -import 'package:mangayomi/models/source.dart'; -import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; -import 'package:mangayomi/services/isolate_service.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'search_.g.dart'; - -@riverpod -Future search( - Ref ref, { - required Source source, - required String query, - required int page, - required List filterList, -}) async { - if (source.name == "local" && source.lang == "") { - final result = - (await isar.mangas - .filter() - .group( - (q) => q - .sourceEqualTo("local") - .or() - .linkContains("Mangayomi/local") - .or() - .linkContains("Mangayomi\\local"), - ) - .nameContains(query, caseSensitive: false) - .offset(page * 50) - .limit(50) - .findAll()) - .map((e) => MManga(name: e.name)) - .toList(); - return MPages(list: result, hasNextPage: true); - } - return getIsolateService.get( - query: query, - filterList: filterList, - source: source, - page: page, - serviceType: 'search', - proxyServer: ref.read(androidProxyServerStateProvider), - ); -} diff --git a/lib/services/search_.g.dart b/lib/services/search_.g.dart deleted file mode 100644 index 7d76a639..00000000 --- a/lib/services/search_.g.dart +++ /dev/null @@ -1,109 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'search_.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, type=warning - -@ProviderFor(search) -final searchProvider = SearchFamily._(); - -final class SearchProvider - extends $FunctionalProvider, MPages?, FutureOr> - with $FutureModifier, $FutureProvider { - SearchProvider._({ - required SearchFamily super.from, - required ({Source source, String query, int page, List filterList}) - super.argument, - }) : super( - retry: null, - name: r'searchProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$searchHash(); - - @override - String toString() { - return r'searchProvider' - '' - '$argument'; - } - - @$internal - @override - $FutureProviderElement $createElement($ProviderPointer pointer) => - $FutureProviderElement(pointer); - - @override - FutureOr create(Ref ref) { - final argument = - this.argument - as ({ - Source source, - String query, - int page, - List filterList, - }); - return search( - ref, - source: argument.source, - query: argument.query, - page: argument.page, - filterList: argument.filterList, - ); - } - - @override - bool operator ==(Object other) { - return other is SearchProvider && other.argument == argument; - } - - @override - int get hashCode { - return argument.hashCode; - } -} - -String _$searchHash() => r'0fa9d882436b1b58b3420dae5a757e7622273eb5'; - -final class SearchFamily extends $Family - with - $FunctionalFamilyOverride< - FutureOr, - ({Source source, String query, int page, List filterList}) - > { - SearchFamily._() - : super( - retry: null, - name: r'searchProvider', - dependencies: null, - $allTransitiveDependencies: null, - isAutoDispose: true, - ); - - SearchProvider call({ - required Source source, - required String query, - required int page, - required List filterList, - }) => SearchProvider._( - argument: ( - source: source, - query: query, - page: page, - filterList: filterList, - ), - from: this, - ); - - @override - String toString() => r'searchProvider'; -} From 489a19170ffc2aa8d7ae682c54036ad497fd2213 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:31:18 +0200 Subject: [PATCH 08/20] Fix and improve ChapterTransitionPage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix an Exception: ``` ════════ Exception caught by rendering library ═════════════════════════════════ A RenderFlex overflowed by 274 pixels on the bottom. The relevant error-causing widget was: Column Column:file:///lib/modules/manga/reader/widgets/chapter_transition_page.dart:28:18 ════════════════════════════════════════════════════════════════════════════════ ``` Improved: The UI adapts to the reading mode now --- .../widgets/chapter_transition_page.dart | 478 +++++++++++------- .../reader/widgets/transition_view_paged.dart | 6 + .../widgets/transition_view_vertical.dart | 6 + 3 files changed, 305 insertions(+), 185 deletions(-) diff --git a/lib/modules/manga/reader/widgets/chapter_transition_page.dart b/lib/modules/manga/reader/widgets/chapter_transition_page.dart index 2965e98f..3562ae2f 100644 --- a/lib/modules/manga/reader/widgets/chapter_transition_page.dart +++ b/lib/modules/manga/reader/widgets/chapter_transition_page.dart @@ -1,216 +1,324 @@ import 'package:flutter/material.dart'; import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; class ChapterTransitionPage extends StatelessWidget { final Chapter currentChapter; final Chapter? nextChapter; final String mangaName; + final ReaderMode readerMode; const ChapterTransitionPage({ super.key, required this.currentChapter, required this.nextChapter, required this.mangaName, + required this.readerMode, }); + bool get _isVertical => + readerMode == ReaderMode.vertical || + readerMode == ReaderMode.verticalContinuous || + readerMode == ReaderMode.webtoon; + + bool get _isRTL => + readerMode == ReaderMode.rtl || + readerMode == ReaderMode.horizontalContinuousRTL; + @override Widget build(BuildContext context) { - final screenHeight = MediaQuery.of(context).size.height; - final screenWidth = MediaQuery.of(context).size.width; - final l10n = context.l10n; return Container( - constraints: BoxConstraints(minHeight: screenHeight * 0.5), color: Theme.of(context).scaffoldBackgroundColor, child: Center( - child: Padding( - padding: EdgeInsets.all(screenWidth * 0.08), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // Icône de transition - Icon( - Icons.auto_stories_outlined, - size: screenWidth * 0.16, - color: Theme.of(context).colorScheme.primary, - ), - - SizedBox(height: screenHeight * 0.04), - - // Titre - Text( - l10n.end_of_chapter, - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - - SizedBox(height: screenHeight * 0.03), - - Container( - padding: EdgeInsets.all(screenWidth * 0.04), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surface, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withValues(alpha: 0.2), + child: LayoutBuilder( + builder: (context, constraints) { + return FittedBox( + fit: BoxFit.scaleDown, + child: ConstrainedBox( + // Give the content a natural maximum size to fit within. + // FittedBox will scale it down if the available space is smaller. + constraints: BoxConstraints( + maxWidth: _isVertical + ? constraints.maxWidth.clamp(100.0, 480.0) + : constraints.maxWidth.clamp(100.0, double.infinity), + maxHeight: constraints.maxHeight.clamp( + 100.0, + double.infinity, ), ), - child: Column( - children: [ - Text( - l10n.chapter_completed, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - SizedBox(height: screenHeight * 0.01), - Text( - currentChapter.name ?? 'Chapitre ${currentChapter.id}', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], + child: Padding( + padding: const EdgeInsets.all(24.0), + child: _isVertical + ? _buildVerticalLayout(context) + : _buildHorizontalLayout(context), ), ), + ); + }, + ), + ), + ); + } - SizedBox(height: screenHeight * 0.03), + // ── Vertical layout (top → arrow → bottom) ──────────────────────────────── - Icon( - nextChapter != null - ? Icons.keyboard_arrow_down - : Icons.check_circle_outline, - size: screenWidth * 0.08, - color: nextChapter != null - ? Theme.of(context).colorScheme.primary - : Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - ), - - SizedBox(height: screenHeight * 0.03), - - if (nextChapter != null) ...[ - Container( - padding: EdgeInsets.all(screenWidth * 0.04), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.3), - ), - ), - child: Column( - children: [ - Text( - l10n.next_chapter, - style: Theme.of(context).textTheme.labelMedium - ?.copyWith( - color: Theme.of(context) - .colorScheme - .onPrimaryContainer - .withValues(alpha: 0.8), - ), - ), - SizedBox(height: screenHeight * 0.01), - Text( - nextChapter!.name ?? 'Chapitre ${nextChapter!.id}', - style: Theme.of(context).textTheme.titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - ), - textAlign: TextAlign.center, - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - SizedBox(height: screenHeight * 0.04), - Text( - l10n.continue_to_next_chapter, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - ] else ...[ - Container( - padding: EdgeInsets.all(screenWidth * 0.04), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.outline.withValues(alpha: 0.3), - ), - ), - child: Column( - children: [ - Icon( - Icons.last_page, - size: screenWidth * 0.06, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.7), - ), - SizedBox(height: screenHeight * 0.01), - Text( - l10n.no_next_chapter, - style: Theme.of(context).textTheme.titleMedium - ?.copyWith( - fontWeight: FontWeight.w600, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.8), - ), - textAlign: TextAlign.center, - ), - SizedBox(height: screenHeight * 0.005), - Text( - l10n.you_have_finished_reading, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - SizedBox(height: screenHeight * 0.04), - Text( - l10n.return_to_the_list_of_chapters, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), - ), - textAlign: TextAlign.center, - ), - ], - ], + Widget _buildVerticalLayout(BuildContext context) { + final l10n = context.l10n; + return IntrinsicWidth( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.auto_stories_outlined, + size: 48, + color: Theme.of(context).colorScheme.primary, ), + const SizedBox(height: 20), + Text( + l10n.end_of_chapter, + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + _buildChapterCard( + context, + label: l10n.chapter_completed, + name: currentChapter.name ?? 'Chapter ${currentChapter.id}', + isPrimary: false, + ), + const SizedBox(height: 16), + Icon( + nextChapter != null + ? Icons.keyboard_arrow_down + : Icons.check_circle_outline, + size: 32, + color: nextChapter != null + ? Theme.of(context).colorScheme.primary + : Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + const SizedBox(height: 16), + if (nextChapter != null) ...[ + _buildChapterCard( + context, + label: l10n.next_chapter, + name: nextChapter!.name ?? 'Chapter ${nextChapter!.id}', + isPrimary: true, + ), + const SizedBox(height: 20), + Text( + l10n.continue_to_next_chapter, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + ] else ...[ + _buildEndOfMangaCard(context), + const SizedBox(height: 20), + Text( + l10n.return_to_the_list_of_chapters, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + ], + ], + ), + ); + } + + // ── Horizontal layout (left → arrow → right) ────────────────────────────── + + Widget _buildHorizontalLayout(BuildContext context) { + final l10n = context.l10n; + + // For LTR: [current] → [next] + // For RTL: [next] ← [current] + final Widget currentCard = _buildChapterCard( + context, + label: l10n.chapter_completed, + name: currentChapter.name ?? 'Chapter ${currentChapter.id}', + isPrimary: false, + width: 200, + ); + + final Widget arrowIcon = Icon( + nextChapter != null + ? (_isRTL ? Icons.keyboard_arrow_left : Icons.keyboard_arrow_right) + : Icons.check_circle_outline, + size: 36, + color: nextChapter != null + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ); + + final Widget nextCard = nextChapter != null + ? _buildChapterCard( + context, + label: l10n.next_chapter, + name: nextChapter!.name ?? 'Chapter ${nextChapter!.id}', + isPrimary: true, + width: 200, + ) + : _buildEndOfMangaCard(context, width: 200); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.auto_stories_outlined, + size: 40, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 16), + Text( + l10n.end_of_chapter, + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: _isRTL + ? [ + nextCard, + const SizedBox(width: 12), + arrowIcon, + const SizedBox(width: 12), + currentCard, + ] + : [ + currentCard, + const SizedBox(width: 12), + arrowIcon, + const SizedBox(width: 12), + nextCard, + ], + ), + const SizedBox(height: 16), + Text( + nextChapter != null + ? l10n.continue_to_next_chapter + : l10n.return_to_the_list_of_chapters, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of( + context, + ).colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + ], + ); + } + + // ── Shared card widgets ──────────────────────────────────────────────────── + + Widget _buildChapterCard( + BuildContext context, { + required String label, + required String name, + required bool isPrimary, + double? width, + }) { + final theme = Theme.of(context); + final bgColor = isPrimary + ? theme.colorScheme.primaryContainer + : theme.colorScheme.surface; + final borderColor = isPrimary + ? theme.colorScheme.primary.withValues(alpha: 0.3) + : theme.colorScheme.outline.withValues(alpha: 0.2); + final labelColor = isPrimary + ? theme.colorScheme.onPrimaryContainer.withValues(alpha: 0.8) + : theme.colorScheme.onSurface.withValues(alpha: 0.7); + final nameColor = isPrimary ? theme.colorScheme.onPrimaryContainer : null; + + return SizedBox( + width: width, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: bgColor, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: borderColor), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + label, + style: theme.textTheme.labelMedium?.copyWith(color: labelColor), + ), + const SizedBox(height: 6), + Text( + name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: nameColor, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + Widget _buildEndOfMangaCard(BuildContext context, {double? width}) { + final l10n = context.l10n; + final theme = Theme.of(context); + return SizedBox( + width: width, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.colorScheme.outline.withValues(alpha: 0.3), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.last_page, + size: 24, + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), + const SizedBox(height: 6), + Text( + l10n.no_next_chapter, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurface.withValues(alpha: 0.8), + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 4), + Text( + l10n.you_have_finished_reading, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), + ), + textAlign: TextAlign.center, + ), + ], ), ), ); diff --git a/lib/modules/manga/reader/widgets/transition_view_paged.dart b/lib/modules/manga/reader/widgets/transition_view_paged.dart index 57f8bb75..d8dbf226 100644 --- a/lib/modules/manga/reader/widgets/transition_view_paged.dart +++ b/lib/modules/manga/reader/widgets/transition_view_paged.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; import 'package:mangayomi/modules/manga/reader/widgets/chapter_transition_page.dart'; @@ -14,10 +15,15 @@ class TransitionViewPaged extends ConsumerWidget { return const SizedBox.shrink(); } + final readerMode = ref + .read(readerControllerProvider(chapter: data.chapter!).notifier) + .getReaderMode(); + return ChapterTransitionPage( currentChapter: data.chapter!, nextChapter: data.nextChapter, mangaName: data.mangaName ?? '', + readerMode: readerMode, ); } } diff --git a/lib/modules/manga/reader/widgets/transition_view_vertical.dart b/lib/modules/manga/reader/widgets/transition_view_vertical.dart index 2a80e717..5e0f233a 100644 --- a/lib/modules/manga/reader/widgets/transition_view_vertical.dart +++ b/lib/modules/manga/reader/widgets/transition_view_vertical.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; import 'package:mangayomi/modules/manga/reader/widgets/chapter_transition_page.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; @@ -15,12 +16,17 @@ class TransitionViewVertical extends ConsumerWidget { return const SizedBox.shrink(); } + final readerMode = ref + .read(readerControllerProvider(chapter: data.chapter!).notifier) + .getReaderMode(); + return SizedBox( height: context.height(1), child: ChapterTransitionPage( currentChapter: data.chapter!, nextChapter: data.nextChapter, mangaName: data.mangaName ?? '', + readerMode: readerMode, ), ); } From defb63f2df15383638ec03449c43318771607995 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:11:43 +0200 Subject: [PATCH 09/20] Redundant GetChapterPagesModel created per page Each UChapDataPreload holds a GetChapterPagesModel that itself holds all pageUrls, archiveImages, etc. For a 50-page chapter this creates 50 model objects each referencing all 50 URLs. --- lib/services/get_chapter_pages.dart | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/services/get_chapter_pages.dart b/lib/services/get_chapter_pages.dart index d6653752..22cbd6fe 100644 --- a/lib/services/get_chapter_pages.dart +++ b/lib/services/get_chapter_pages.dart @@ -40,7 +40,6 @@ Future getChapterPages( }) async { final keepAlive = ref.keepAlive(); try { - List uChapDataPreloadp = []; Directory? path; List pageUrls = []; List isLocaleList = []; @@ -86,6 +85,14 @@ Future getChapterPages( } } + final chapterModel = GetChapterPagesModel( + path: path, + pageUrls: pageUrls, + isLocaleList: isLocaleList, + archiveImages: archiveImages, + uChapDataPreload: [], + ); + if (pageUrls.isNotEmpty || isLocalArchive) { if (await File( p.join(mangaDirectory!.path, "${chapter.name}.cbz"), @@ -144,7 +151,7 @@ Future getChapterPages( }); } for (var i = 0; i < pageUrls.length; i++) { - uChapDataPreloadp.add( + chapterModel.uChapDataPreload.add( UChapDataPreload( chapter, path, @@ -152,26 +159,14 @@ Future getChapterPages( isLocaleList[i], archiveImages[i], i, - GetChapterPagesModel( - path: path, - pageUrls: pageUrls, - isLocaleList: isLocaleList, - archiveImages: archiveImages, - uChapDataPreload: uChapDataPreloadp, - ), + chapterModel, i, ), ); } } keepAlive.close(); - return GetChapterPagesModel( - path: path, - pageUrls: pageUrls, - isLocaleList: isLocaleList, - archiveImages: archiveImages, - uChapDataPreload: uChapDataPreloadp, - ); + return chapterModel; } catch (e) { keepAlive.close(); rethrow; From 45a6633344c7eda2fca6a5a6f4d2f534f4f592df Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:36:45 +0200 Subject: [PATCH 10/20] Update manga.dart implement suggestion by Copilot. https://github.com/kodjodevf/mangayomi/pull/703#discussion_r3093233926 --- lib/utils/extensions/manga.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/utils/extensions/manga.dart b/lib/utils/extensions/manga.dart index 2d2f92ce..12ee8a2c 100644 --- a/lib/utils/extensions/manga.dart +++ b/lib/utils/extensions/manga.dart @@ -96,6 +96,7 @@ extension MangaExtensions on Manga { if (a.dateUpload == null || b.dateUpload == null) return 0; return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); }); + break; case 1: // by chapter number - reading list is already ascending break; case 2: // by upload date @@ -103,11 +104,13 @@ extension MangaExtensions on Manga { if (a.dateUpload == null || b.dateUpload == null) return 0; return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); }); + break; case 3: // by name list.sort((a, b) { if (a.name == null || b.name == null) return 0; return a.name!.compareTo(b.name!); }); + break; } return reverse ? list.reversed.toList() : list; From 47ed3cbef96ba047f792f2c3748df6756d0a85a8 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:43:06 +0200 Subject: [PATCH 11/20] Update manga.dart implement suggestion by Copilot. https://github.com/kodjodevf/mangayomi/pull/703#discussion_r3093233947 --- lib/utils/extensions/manga.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/utils/extensions/manga.dart b/lib/utils/extensions/manga.dart index 12ee8a2c..6ad7bd46 100644 --- a/lib/utils/extensions/manga.dart +++ b/lib/utils/extensions/manga.dart @@ -94,7 +94,9 @@ extension MangaExtensions on Manga { final s = a.scanlator!.compareTo(b.scanlator!); if (s != 0) return s; if (a.dateUpload == null || b.dateUpload == null) return 0; - return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); + return (int.tryParse(a.dateUpload!) ?? 0).compareTo( + int.tryParse(b.dateUpload!) ?? 0, + ); }); break; case 1: // by chapter number - reading list is already ascending @@ -102,7 +104,9 @@ extension MangaExtensions on Manga { case 2: // by upload date list.sort((a, b) { if (a.dateUpload == null || b.dateUpload == null) return 0; - return int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); + return (int.tryParse(a.dateUpload!) ?? 0).compareTo( + int.tryParse(b.dateUpload!) ?? 0, + ); }); break; case 3: // by name From 0cfc8456b776e2da4ad0d8a196f5c11ce7adf52d Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:21:49 +0200 Subject: [PATCH 12/20] set _isNextChapterPreloading = false if it isn't --- lib/modules/manga/reader/reader_view.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 986765b4..0e578d01 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -1061,7 +1061,10 @@ class _MangaChapterPageGalleryState if (_isNextChapterPreloading || _isLastPageTransition) return; _isNextChapterPreloading = true; try { - if (!mounted) return; + if (!mounted) { + _isNextChapterPreloading = false; + return; + } final nextChapter = _readerController.getNextChapter(); if (isChapterLoaded(nextChapter)) { _isNextChapterPreloading = false; From 1eed6fe01ee7bde74cc28fc99a92e2810d2534a5 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sat, 18 Apr 2026 20:15:03 +0200 Subject: [PATCH 13/20] add ordered prefetch to prioritize early pages --- lib/modules/manga/reader/reader_view.dart | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 0e578d01..ec35a06c 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -24,6 +24,7 @@ import 'package:mangayomi/modules/manga/reader/widgets/page_indicator.dart'; import 'package:mangayomi/modules/manga/reader/widgets/image_actions_dialog.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/utils/extensions/others.dart'; import 'package:mangayomi/utils/riverpod.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; import 'package:mangayomi/services/get_chapter_pages.dart'; @@ -1170,6 +1171,10 @@ class _MangaChapterPageGalleryState }, ); + // Kick off ordered prefetch before the first frame so lower-indexed pages + // win the HTTP race against the simultaneous widget-driven loads. + _prefetchPagesInOrder(); // intentionally not awaited + // proactively start loading adjacent chapters in background _proactivePreload(); @@ -1205,6 +1210,46 @@ class _MangaChapterPageGalleryState } } + /// Warms Flutter's [ImageCache] in page order before the widget tree renders. + /// + /// [ScrollablePositionedList] builds all items within [minCacheExtent] in a + /// single frame, firing every network request simultaneously, which means + /// pages complete in arbitrary (server-response) order. By resolving each + /// provider sequentially here — starting before that first frame — we seed + /// the cache so that earlier pages win the HTTP race: lower-indexed pages + /// start their requests first and are therefore ready sooner. + /// + /// For pages already within the cache extent the widget will attach to the + /// already-pending Future (Flutter deduplicates by provider key), so no + /// extra requests are made. Pages beyond the cache extent are fetched + /// strictly one at a time in reading order, so the reader never sees a + /// later page appear before an earlier one. + /// + /// This is fully async — [await] inside a fire-and-forget call — so the + /// UI stays interactive throughout. + Future _prefetchPagesInOrder() async { + final startIdx = (_currentIndex ?? 0).clamp(0, pages.length - 1); + + // Visit pages from the opening position forward, then backward. + final indices = [ + for (var i = startIdx; i < pages.length; i++) i, + for (var i = startIdx - 1; i >= 0; i--) i, + ]; + + for (final i in indices) { + if (!mounted) return; + final page = pages[i]; + if (page.isTransitionPage) continue; + try { + // Awaiting ensures page[i] finishes (or fails) before page[i+1] + // starts downloading, giving strict reading-order priority. + await precacheImage(page.getImageProvider(ref, true), context); + } catch (_) { + // Swallow errors: network failures, widget disposal, etc. + } + } + } + Future _onPageChanged(int index) async { // In non-continuous double page mode, convert page view index to actual // pages array index for correct lookups. From 519dd048eea4fc6b7997ee479ca094c060c805f2 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:09:49 +0200 Subject: [PATCH 14/20] Use width for minCacheExtent when in horizontal --- lib/modules/manga/reader/reader_view.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index ec35a06c..feca74dc 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -423,8 +423,9 @@ class _MangaChapterPageGalleryState scrollDirection: isHorizontalContinuous ? Axis.horizontal : Axis.vertical, - minCacheExtent: - pagePreloadAmount * context.height(1), + minCacheExtent: isHorizontalContinuous + ? pagePreloadAmount * context.width(1) + : pagePreloadAmount * context.height(1), initialScrollIndex: _readerController .getPageIndex(), physics: const ClampingScrollPhysics(), From ef31d94e01dba964df7b94dd8a65dcd2d48196cb Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:10:12 +0200 Subject: [PATCH 15/20] Use animatePageTransitions value --- lib/modules/manga/reader/reader_view.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index feca74dc..8624b6a5 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -347,6 +347,9 @@ class _MangaChapterPageGalleryState @override Widget build(BuildContext context) { + final animatePageTransitions = ref.watch( + animatePageTransitionsStateProvider, + ); final backgroundColor = ref.watch(backgroundColorStateProvider); final fullScreenReader = ref.watch(fullScreenReaderStateProvider); final readerMode = ref.watch(_currentReaderMode); @@ -359,13 +362,13 @@ class _MangaChapterPageGalleryState onPreviousPage: () => navigationService.previousPage( readerMode: readerMode!, currentIndex: _currentIndex!, - animate: true, + animate: animatePageTransitions, ), onNextPage: () => navigationService.nextPage( readerMode: readerMode!, currentIndex: _currentIndex!, maxPages: _pageViewPageCount, - animate: true, + animate: animatePageTransitions, ), onEscape: () => _goBack(context), onFullScreen: () => _setFullScreen(), @@ -760,13 +763,13 @@ class _MangaChapterPageGalleryState onPreviousPage: () => navigationService.previousPage( readerMode: readerMode!, currentIndex: _currentIndex!, - animate: true, + animate: animatePageTransitions, ), onNextPage: () => navigationService.nextPage( readerMode: readerMode!, currentIndex: _currentIndex!, maxPages: _pageViewPageCount, - animate: true, + animate: animatePageTransitions, ), onDoubleTapDown: (position) => _toggleScale(position), onDoubleTap: () {}, From e68815bde28ffe57e5929b4cd718cac9537ba71d Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:09:48 +0200 Subject: [PATCH 16/20] Fix Overflow Exception --- .../widgets/chapter_transition_page.dart | 40 +++++++++---------- 1 file changed, 18 insertions(+), 22 deletions(-) diff --git a/lib/modules/manga/reader/widgets/chapter_transition_page.dart b/lib/modules/manga/reader/widgets/chapter_transition_page.dart index 3562ae2f..e0c43397 100644 --- a/lib/modules/manga/reader/widgets/chapter_transition_page.dart +++ b/lib/modules/manga/reader/widgets/chapter_transition_page.dart @@ -142,6 +142,7 @@ class ChapterTransitionPage extends StatelessWidget { Widget _buildHorizontalLayout(BuildContext context) { final l10n = context.l10n; + final theme = Theme.of(context); // For LTR: [current] → [next] // For RTL: [next] ← [current] @@ -150,7 +151,6 @@ class ChapterTransitionPage extends StatelessWidget { label: l10n.chapter_completed, name: currentChapter.name ?? 'Chapter ${currentChapter.id}', isPrimary: false, - width: 200, ); final Widget arrowIcon = Icon( @@ -159,8 +159,8 @@ class ChapterTransitionPage extends StatelessWidget { : Icons.check_circle_outline, size: 36, color: nextChapter != null - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.6), + ? theme.colorScheme.primary + : theme.colorScheme.onSurface.withValues(alpha: 0.6), ); final Widget nextCard = nextChapter != null @@ -169,9 +169,8 @@ class ChapterTransitionPage extends StatelessWidget { label: l10n.next_chapter, name: nextChapter!.name ?? 'Chapter ${nextChapter!.id}', isPrimary: true, - width: 200, ) - : _buildEndOfMangaCard(context, width: 200); + : _buildEndOfMangaCard(context); return Column( mainAxisSize: MainAxisSize.min, @@ -179,14 +178,14 @@ class ChapterTransitionPage extends StatelessWidget { Icon( Icons.auto_stories_outlined, size: 40, - color: Theme.of(context).colorScheme.primary, + color: theme.colorScheme.primary, ), const SizedBox(height: 16), Text( l10n.end_of_chapter, - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), textAlign: TextAlign.center, ), const SizedBox(height: 20), @@ -195,18 +194,18 @@ class ChapterTransitionPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: _isRTL ? [ - nextCard, + Flexible(child: nextCard), const SizedBox(width: 12), arrowIcon, const SizedBox(width: 12), - currentCard, + Flexible(child: currentCard), ] : [ - currentCard, + Flexible(child: currentCard), const SizedBox(width: 12), arrowIcon, const SizedBox(width: 12), - nextCard, + Flexible(child: nextCard), ], ), const SizedBox(height: 16), @@ -214,10 +213,8 @@ class ChapterTransitionPage extends StatelessWidget { nextChapter != null ? l10n.continue_to_next_chapter : l10n.return_to_the_list_of_chapters, - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.6), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.6), ), textAlign: TextAlign.center, ), @@ -232,7 +229,6 @@ class ChapterTransitionPage extends StatelessWidget { required String label, required String name, required bool isPrimary, - double? width, }) { final theme = Theme.of(context); final bgColor = isPrimary @@ -247,7 +243,6 @@ class ChapterTransitionPage extends StatelessWidget { final nameColor = isPrimary ? theme.colorScheme.onPrimaryContainer : null; return SizedBox( - width: width, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( @@ -260,7 +255,9 @@ class ChapterTransitionPage extends StatelessWidget { children: [ Text( label, + textAlign: TextAlign.center, style: theme.textTheme.labelMedium?.copyWith(color: labelColor), + maxLines: 2, ), const SizedBox(height: 6), Text( @@ -270,7 +267,7 @@ class ChapterTransitionPage extends StatelessWidget { color: nameColor, ), textAlign: TextAlign.center, - maxLines: 2, + maxLines: 3, overflow: TextOverflow.ellipsis, ), ], @@ -279,11 +276,10 @@ class ChapterTransitionPage extends StatelessWidget { ); } - Widget _buildEndOfMangaCard(BuildContext context, {double? width}) { + Widget _buildEndOfMangaCard(BuildContext context) { final l10n = context.l10n; final theme = Theme.of(context); return SizedBox( - width: width, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( From af49eaee687a7561bc844120a3d22f4c3cbf5630 Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Mon, 20 Apr 2026 02:33:50 +0200 Subject: [PATCH 17/20] Make cards equal in size --- .../widgets/chapter_transition_page.dart | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/lib/modules/manga/reader/widgets/chapter_transition_page.dart b/lib/modules/manga/reader/widgets/chapter_transition_page.dart index e0c43397..a9819c61 100644 --- a/lib/modules/manga/reader/widgets/chapter_transition_page.dart +++ b/lib/modules/manga/reader/widgets/chapter_transition_page.dart @@ -189,24 +189,26 @@ class ChapterTransitionPage extends StatelessWidget { textAlign: TextAlign.center, ), const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: _isRTL - ? [ - Flexible(child: nextCard), - const SizedBox(width: 12), - arrowIcon, - const SizedBox(width: 12), - Flexible(child: currentCard), - ] - : [ - Flexible(child: currentCard), - const SizedBox(width: 12), - arrowIcon, - const SizedBox(width: 12), - Flexible(child: nextCard), - ], + IntrinsicHeight( + child: Row( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: _isRTL + ? [ + Expanded(child: nextCard), + const SizedBox(width: 12), + Center(child: arrowIcon), + const SizedBox(width: 12), + Expanded(child: currentCard), + ] + : [ + Expanded(child: currentCard), + const SizedBox(width: 12), + Center(child: arrowIcon), + const SizedBox(width: 12), + Expanded(child: nextCard), + ], + ), ), const SizedBox(height: 16), Text( @@ -252,6 +254,7 @@ class ChapterTransitionPage extends StatelessWidget { ), child: Column( mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( label, From a0512564b4c80d8a10ea03ac5b3f885e72bd3b77 Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:58:45 +0100 Subject: [PATCH 18/20] Disable previous chapter preloading for optimization --- lib/modules/manga/reader/reader_view.dart | 148 +++++++++++----------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 8624b6a5..8da71871 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -1006,10 +1006,10 @@ class _MangaChapterPageGalleryState _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) { @@ -1058,7 +1058,7 @@ class _MangaChapterPageGalleryState /// Proactively starts loading both adjacent chapters at reader init. void _proactivePreload() { _triggerNextChapterPreload(); - _triggerPrevChapterPreload(); + // _triggerPrevChapterPreload(); } /// Fires off next-chapter page fetching if not already in progress. @@ -1089,76 +1089,76 @@ class _MangaChapterPageGalleryState _isNextChapterPreloading = false; } } + // TODO: Need more optimization + // /// Fires off previous-chapter page fetching and prepends pages. + // void _triggerPrevChapterPreload() async { + // if (_isPrevChapterPreloading) return; + // _isPrevChapterPreloading = true; + // try { + // if (!mounted) return; + // final prevChapter = _readerController.getPrevChapter(); + // if (isChapterLoaded(prevChapter)) { + // _isPrevChapterPreloading = false; + // return; + // } + // final value = await ref.read( + // getChapterPagesProvider(chapter: prevChapter).future, + // ); + // if (mounted) { + // _handlePrevChapterPrepended(value, chapter); + // } + // } on RangeError { + // // No previous chapter — nothing to prepend + // } catch (_) {} + // _isPrevChapterPreloading = false; + // } - /// Fires off previous-chapter page fetching and prepends pages. - void _triggerPrevChapterPreload() async { - if (_isPrevChapterPreloading) return; - _isPrevChapterPreloading = true; - try { - if (!mounted) return; - final prevChapter = _readerController.getPrevChapter(); - if (isChapterLoaded(prevChapter)) { - _isPrevChapterPreloading = false; - return; - } - final value = await ref.read( - getChapterPagesProvider(chapter: prevChapter).future, - ); - if (mounted) { - _handlePrevChapterPrepended(value, chapter); - } - } on RangeError { - // No previous chapter — nothing to prepend - } catch (_) {} - _isPrevChapterPreloading = false; - } + // /// Prepends previous-chapter pages and adjusts scroll position to avoid jump. + // void _handlePrevChapterPrepended( + // GetChapterPagesModel chapterData, + // Chapter chap, + // ) { + // try { + // if (chapterData.uChapDataPreload.isEmpty || !mounted) return; - /// Prepends previous-chapter pages and adjusts scroll position to avoid jump. - void _handlePrevChapterPrepended( - GetChapterPagesModel chapterData, - Chapter chap, - ) { - 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; - // 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; - 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; - // 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(oldTopIndex); - final newActual = oldActual + prependCount; - _currentIndex = _actualToPageViewIndex(newActual); - } else { - _currentIndex = newIndex; - } - setState(() {}); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - if (_isContinuousMode()) { - _itemScrollController.jumpTo(index: newIndex); - } else if (_extendedController.hasClients) { - _extendedController.jumpToPage(_currentIndex!); - } - _isAdjustingScroll = false; - } - }); - } - }); - } catch (_) {} - } + // // 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(oldTopIndex); + // final newActual = oldActual + prependCount; + // _currentIndex = _actualToPageViewIndex(newActual); + // } else { + // _currentIndex = newIndex; + // } + // setState(() {}); + // WidgetsBinding.instance.addPostFrameCallback((_) { + // if (mounted) { + // if (_isContinuousMode()) { + // _itemScrollController.jumpTo(index: newIndex); + // } else if (_extendedController.hasClients) { + // _extendedController.jumpToPage(_currentIndex!); + // } + // _isAdjustingScroll = false; + // } + // }); + // } + // }); + // } catch (_) {} + // } void _initCurrentIndex() async { if (ref.read(cropBordersStateProvider)) _processCropBorders(); @@ -1309,10 +1309,10 @@ class _MangaChapterPageGalleryState _triggerNextChapterPreload(); } - // ── Previous-chapter preloading: trigger when near the start ── - if (actualIndex <= pagePreloadAmount) { - _triggerPrevChapterPreload(); - } + // // ── Previous-chapter preloading: trigger when near the start ── + // if (actualIndex <= pagePreloadAmount) { + // _triggerPrevChapterPreload(); + // } } late final _pageOffset = ValueNotifier( From b5de2693bac789c9e2d54066073fc82359ae21f8 Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Tue, 21 Apr 2026 10:08:33 +0100 Subject: [PATCH 19/20] Revert "Disable previous chapter preloading for optimization" This reverts commit a0512564b4c80d8a10ea03ac5b3f885e72bd3b77. --- lib/modules/manga/reader/reader_view.dart | 148 +++++++++++----------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 8da71871..8624b6a5 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -1006,10 +1006,10 @@ class _MangaChapterPageGalleryState _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) { @@ -1058,7 +1058,7 @@ class _MangaChapterPageGalleryState /// Proactively starts loading both adjacent chapters at reader init. void _proactivePreload() { _triggerNextChapterPreload(); - // _triggerPrevChapterPreload(); + _triggerPrevChapterPreload(); } /// Fires off next-chapter page fetching if not already in progress. @@ -1089,76 +1089,76 @@ class _MangaChapterPageGalleryState _isNextChapterPreloading = false; } } - // TODO: Need more optimization - // /// Fires off previous-chapter page fetching and prepends pages. - // void _triggerPrevChapterPreload() async { - // if (_isPrevChapterPreloading) return; - // _isPrevChapterPreloading = true; - // try { - // if (!mounted) return; - // final prevChapter = _readerController.getPrevChapter(); - // if (isChapterLoaded(prevChapter)) { - // _isPrevChapterPreloading = false; - // return; - // } - // final value = await ref.read( - // getChapterPagesProvider(chapter: prevChapter).future, - // ); - // if (mounted) { - // _handlePrevChapterPrepended(value, chapter); - // } - // } on RangeError { - // // No previous chapter — nothing to prepend - // } catch (_) {} - // _isPrevChapterPreloading = false; - // } - // /// Prepends previous-chapter pages and adjusts scroll position to avoid jump. - // void _handlePrevChapterPrepended( - // GetChapterPagesModel chapterData, - // Chapter chap, - // ) { - // try { - // if (chapterData.uChapDataPreload.isEmpty || !mounted) return; + /// Fires off previous-chapter page fetching and prepends pages. + void _triggerPrevChapterPreload() async { + if (_isPrevChapterPreloading) return; + _isPrevChapterPreloading = true; + try { + if (!mounted) return; + final prevChapter = _readerController.getPrevChapter(); + if (isChapterLoaded(prevChapter)) { + _isPrevChapterPreloading = false; + return; + } + final value = await ref.read( + getChapterPagesProvider(chapter: prevChapter).future, + ); + if (mounted) { + _handlePrevChapterPrepended(value, chapter); + } + } on RangeError { + // No previous chapter — nothing to prepend + } catch (_) {} + _isPrevChapterPreloading = false; + } - // // Record the CURRENT visible top index BEFORE prepending - // final currentVisibleItems = _itemPositionsListener.itemPositions.value; - // final oldTopIndex = currentVisibleItems.isNotEmpty - // ? currentVisibleItems.first.index - // : _currentIndex ?? 0; + /// Prepends previous-chapter pages and adjusts scroll position to avoid jump. + void _handlePrevChapterPrepended( + GetChapterPagesModel chapterData, + Chapter chap, + ) { + try { + if (chapterData.uChapDataPreload.isEmpty || !mounted) return; - // preloadPreviousChapter(chapterData, chap).then((prependCount) { - // if (prependCount > 0 && mounted) { - // _isAdjustingScroll = true; + // Record the CURRENT visible top index BEFORE prepending + final currentVisibleItems = _itemPositionsListener.itemPositions.value; + final oldTopIndex = currentVisibleItems.isNotEmpty + ? currentVisibleItems.first.index + : _currentIndex ?? 0; - // // New index = old visible index + how many items we just prepended - // final newIndex = oldTopIndex + prependCount; + preloadPreviousChapter(chapterData, chap).then((prependCount) { + if (prependCount > 0 && mounted) { + _isAdjustingScroll = true; - // // 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(oldTopIndex); - // final newActual = oldActual + prependCount; - // _currentIndex = _actualToPageViewIndex(newActual); - // } else { - // _currentIndex = newIndex; - // } - // setState(() {}); - // WidgetsBinding.instance.addPostFrameCallback((_) { - // if (mounted) { - // if (_isContinuousMode()) { - // _itemScrollController.jumpTo(index: newIndex); - // } else if (_extendedController.hasClients) { - // _extendedController.jumpToPage(_currentIndex!); - // } - // _isAdjustingScroll = false; - // } - // }); - // } - // }); - // } catch (_) {} - // } + // 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(oldTopIndex); + final newActual = oldActual + prependCount; + _currentIndex = _actualToPageViewIndex(newActual); + } else { + _currentIndex = newIndex; + } + setState(() {}); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + if (_isContinuousMode()) { + _itemScrollController.jumpTo(index: newIndex); + } else if (_extendedController.hasClients) { + _extendedController.jumpToPage(_currentIndex!); + } + _isAdjustingScroll = false; + } + }); + } + }); + } catch (_) {} + } void _initCurrentIndex() async { if (ref.read(cropBordersStateProvider)) _processCropBorders(); @@ -1309,10 +1309,10 @@ class _MangaChapterPageGalleryState _triggerNextChapterPreload(); } - // // ── Previous-chapter preloading: trigger when near the start ── - // if (actualIndex <= pagePreloadAmount) { - // _triggerPrevChapterPreload(); - // } + // ── Previous-chapter preloading: trigger when near the start ── + if (actualIndex <= pagePreloadAmount) { + _triggerPrevChapterPreload(); + } } late final _pageOffset = ValueNotifier( From 6fe1b8e84441223a04bc41a2e8e76ae869df9b54 Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:52:43 +0100 Subject: [PATCH 20/20] Refactor chapter navigation and improve page indicator responsiveness --- .../mixins/chapter_controller_mixin.dart | 4 +- lib/modules/manga/reader/reader_view.dart | 44 +++++++--- .../manga/reader/widgets/page_indicator.dart | 43 +++++----- .../reader/widgets/reader_bottom_bar.dart | 82 ++++++++++--------- lib/utils/extensions/manga.dart | 2 + 5 files changed, 102 insertions(+), 73 deletions(-) diff --git a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart index 4080136a..af1100a9 100644 --- a/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart +++ b/lib/modules/manga/reader/mixins/chapter_controller_mixin.dart @@ -44,8 +44,8 @@ mixin ChapterControllerMixin { // --------------------------------------------------------------------------- (int, bool) getChapterIndex() => _chapterIndexWithOffset(0); - Chapter getPrevChapter() => _chapterWithOffset(-1); - Chapter getNextChapter() => _chapterWithOffset(1); + Chapter getPrevChapter() => _chapterWithOffset(1); + Chapter getNextChapter() => _chapterWithOffset(-1); /// Finds this [chapter] in either the filtered list or the raw list and /// returns [index + offset]. The boolean indicates whether the filtered list diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index 8624b6a5..e9454f67 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -155,7 +155,6 @@ class _MangaChapterPageGalleryState bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows; final ValueNotifier _isScrolling = ValueNotifier(false); Timer? _scrollIdleTimer; - bool _firstLaunch = true; final Stopwatch _readingStopwatch = Stopwatch(); /// Flag to prevent fullscreen from being disabled when navigating between @@ -177,6 +176,7 @@ class _MangaChapterPageGalleryState _autoScroll.value = false; _autoScroll.dispose(); _autoScrollPage.dispose(); + _currentPageDisplayIndex.dispose(); _scrollIdleTimer?.cancel(); _isScrolling.dispose(); _itemPositionsListener.itemPositions.removeListener(_readProgressListener); @@ -195,11 +195,11 @@ class _MangaChapterPageGalleryState ); } discordRpc?.showIdleText(); - final actualIdx = _pageViewToActualIndex(_currentIndex!); + final actualIdx = _pageViewToActualIndexSync(_currentIndex!); final index = pages[actualIdx].index; if (index != null) { _readerController.setPageIndex( - _isDoublePageActive ? index : _geCurrentIndex(index), + _isDoublePageActiveSync ? index : _geCurrentIndex(index), true, ); } @@ -238,6 +238,9 @@ class _MangaChapterPageGalleryState final _failedToLoadImage = ValueNotifier(false); late int? _currentIndex = _readerController.getPageIndex(); + late final ValueNotifier _currentPageDisplayIndex = ValueNotifier( + _readerController.getPageIndex(), + ); late final ItemScrollController _itemScrollController = ItemScrollController(); @@ -301,6 +304,9 @@ class _MangaChapterPageGalleryState final _currentReaderMode = StateProvider(() => null); PageMode? _pageMode; bool _isView = false; + + /// Cached reader mode to safely access in dispose without ref.read() + ReaderMode? _cachedReaderMode; Alignment _scalePosition = Alignment.center; final PhotoViewController _photoViewController = PhotoViewController(); final PhotoViewScaleStateController _photoViewScaleStateController = @@ -829,6 +835,7 @@ class _MangaChapterPageGalleryState ); }, onSliderChanged: (value, ref) { + _currentPageDisplayIndex.value = value; ref .read(currentIndexProvider(chapter).notifier) .setCurrentIndex(value); @@ -912,7 +919,7 @@ class _MangaChapterPageGalleryState }, ), currentReaderModeProvider: _currentReaderMode, - currentIndexProvider: currentIndexProvider, + currentPageListenable: _currentPageDisplayIndex, currentPageMode: _pageMode, isReverseHorizontal: _isReverseHorizontal, totalPages: _readerController.getPageLength( @@ -922,8 +929,8 @@ class _MangaChapterPageGalleryState backgroundColor: _backgroundColor, ), PageIndicator( - chapter: chapter, isUiVisible: _isView, + currentPageListenable: _currentPageDisplayIndex, totalPages: _readerController.getPageLength( _chapterUrlModel.pageUrls, ), @@ -1013,6 +1020,7 @@ class _MangaChapterPageGalleryState final idx = pages[_currentIndex!].index; if (idx != null) { + _currentPageDisplayIndex.value = idx; _readerController.setPageIndex( _isDoublePageActive ? idx : _geCurrentIndex(idx), false, @@ -1163,6 +1171,7 @@ class _MangaChapterPageGalleryState void _initCurrentIndex() async { if (ref.read(cropBordersStateProvider)) _processCropBorders(); final readerMode = _readerController.getReaderMode(); + _currentPageDisplayIndex.value = _readerController.getPageIndex(); // Initialize the preload manager with bounded memory (from ReaderMemoryManagement mixin) initializePreloadManager( @@ -1259,17 +1268,10 @@ class _MangaChapterPageGalleryState // pages array index for correct lookups. final int actualIndex = _pageViewToActualIndex(index); final int prevActualIndex = _pageViewToActualIndex(_currentIndex!); - final cropBorders = ref.watch(cropBordersStateProvider); if (cropBorders) { _processCropBordersByIndex(index); } - if (_firstLaunch) { - Future.delayed(const Duration(milliseconds: 100)).then((_) { - _firstLaunch = false; - }); - return; - } final idx = pages[prevActualIndex].index; if (idx != null) { _readerController.setPageIndex( @@ -1298,6 +1300,7 @@ class _MangaChapterPageGalleryState clearGestureDetailsCache(); _currentIndex = index; if (pages[actualIndex].index != null) { + _currentPageDisplayIndex.value = pages[actualIndex].index!; ref .read(currentIndexProvider(chapter).notifier) .setCurrentIndex(pages[actualIndex].index!); @@ -1376,6 +1379,9 @@ class _MangaChapterPageGalleryState _failedToLoadImage.value = false; _readerController.setReaderMode(value); + // Cache the reader mode for safe access in dispose + _cachedReaderMode = value; + int index = _pageViewToActualIndex(_currentIndex!); ref.read(_currentReaderMode.notifier).state = value; if (value == ReaderMode.vertical) { @@ -1503,11 +1509,19 @@ class _MangaChapterPageGalleryState /// Whether double page mode is active (continuous or paged). /// Horizontal continuous mode does NOT use double page layout. + /// Uses ref.read() so cannot be called during dispose. bool get _isDoublePageActive => _pageMode == PageMode.doublePage && ref.read(_currentReaderMode) != ReaderMode.horizontalContinuous && ref.read(_currentReaderMode) != ReaderMode.horizontalContinuousRTL; + /// Safe version of _isDoublePageActive that uses cached reader mode. + /// Safe to call during dispose without Riverpod assertion errors. + bool get _isDoublePageActiveSync => + _pageMode == PageMode.doublePage && + _cachedReaderMode != ReaderMode.horizontalContinuous && + _cachedReaderMode != ReaderMode.horizontalContinuousRTL; + /// Converts a page view index (from ExtendedPageController) to the actual /// index in the [pages] array for double page mode. /// @@ -1519,6 +1533,12 @@ class _MangaChapterPageGalleryState return (pageViewIndex * 2).clamp(0, pages.length - 1); } + /// Safe version that uses cached reader mode for use in dispose. + int _pageViewToActualIndexSync(int pageViewIndex) { + if (!_isDoublePageActiveSync) return pageViewIndex; + return (pageViewIndex * 2).clamp(0, pages.length - 1); + } + /// Converts an actual [pages] array index to a page view index /// for double page mode. int _actualToPageViewIndex(int actualIndex) { diff --git a/lib/modules/manga/reader/widgets/page_indicator.dart b/lib/modules/manga/reader/widgets/page_indicator.dart index 08d0d613..77548807 100644 --- a/lib/modules/manga/reader/widgets/page_indicator.dart +++ b/lib/modules/manga/reader/widgets/page_indicator.dart @@ -1,7 +1,6 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; /// Page indicator widget showing current page / total pages. @@ -9,12 +8,12 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr /// Displayed at the bottom center when the UI is hidden and /// "show page numbers" setting is enabled. class PageIndicator extends ConsumerWidget { - /// The current chapter being read. - final Chapter chapter; - /// Whether the UI overlay is currently visible. final bool isUiVisible; + /// Session-local current page index for the visible reader state. + final ValueListenable currentPageListenable; + /// Total number of pages. final int totalPages; @@ -23,15 +22,14 @@ class PageIndicator extends ConsumerWidget { const PageIndicator({ super.key, - required this.chapter, required this.isUiVisible, + required this.currentPageListenable, required this.totalPages, required this.formatCurrentIndex, }); @override Widget build(BuildContext context, WidgetRef ref) { - final currentIndex = ref.watch(currentIndexProvider(chapter)); final showPagesNumber = ref.watch(showPagesNumberStateProvider); // Don't show when UI is visible or setting is disabled @@ -41,19 +39,24 @@ class PageIndicator extends ConsumerWidget { return Align( alignment: Alignment.bottomCenter, - child: Text( - '${formatCurrentIndex(currentIndex)} / $totalPages', - style: const TextStyle( - color: Colors.white, - fontSize: 20.0, - shadows: [ - Shadow(offset: Offset(-1, -1), blurRadius: 1), - Shadow(offset: Offset(1, -1), blurRadius: 1), - Shadow(offset: Offset(1, 1), blurRadius: 1), - Shadow(offset: Offset(-1, 1), blurRadius: 1), - ], - ), - textAlign: TextAlign.center, + child: ValueListenableBuilder( + valueListenable: currentPageListenable, + builder: (context, currentIndex, child) { + return Text( + '${formatCurrentIndex(currentIndex)} / $totalPages', + style: const TextStyle( + color: Colors.white, + fontSize: 20.0, + shadows: [ + Shadow(offset: Offset(-1, -1), blurRadius: 1), + Shadow(offset: Offset(1, -1), blurRadius: 1), + Shadow(offset: Offset(1, 1), blurRadius: 1), + Shadow(offset: Offset(-1, 1), blurRadius: 1), + ], + ), + textAlign: TextAlign.center, + ); + }, ), ); } diff --git a/lib/modules/manga/reader/widgets/reader_bottom_bar.dart b/lib/modules/manga/reader/widgets/reader_bottom_bar.dart index f4b8eb53..8c93596b 100644 --- a/lib/modules/manga/reader/widgets/reader_bottom_bar.dart +++ b/lib/modules/manga/reader/widgets/reader_bottom_bar.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'dart:math'; import 'package:flutter/cupertino.dart'; @@ -6,7 +7,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/misc.dart' show ProviderListenable; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/settings.dart'; -import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; import 'package:mangayomi/modules/manga/reader/widgets/custom_value_indicator_shape.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; @@ -62,9 +62,8 @@ class ReaderBottomBar extends ConsumerWidget { /// (StateProvider, NotifierProvider, etc.) final ProviderListenable currentReaderModeProvider; - /// Provider family for watching current page index - /// Type: CurrentIndexFamily (from reader_controller_provider.g.dart) - final CurrentIndexFamily currentIndexProvider; + /// Session-local current page index for the visible reader state. + final ValueListenable currentPageListenable; /// Current page mode (nullable for safety) final PageMode? currentPageMode; @@ -95,7 +94,7 @@ class ReaderBottomBar extends ConsumerWidget { this.onPageModeToggle, required this.onSettingsPressed, required this.currentReaderModeProvider, - required this.currentIndexProvider, + required this.currentPageListenable, required this.currentPageMode, required this.isReverseHorizontal, required this.totalPages, @@ -194,15 +193,17 @@ class ReaderBottomBar extends ConsumerWidget { child: Center( child: Consumer( builder: (context, ref, child) { - final currentIndex = ref.watch( - currentIndexProvider(chapter), - ); - return Text( - currentIndexLabel(currentIndex), - style: const TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), + return ValueListenableBuilder( + valueListenable: currentPageListenable, + builder: (context, currentIndex, child) { + return Text( + currentIndexLabel(currentIndex), + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ); + }, ); }, ), @@ -277,34 +278,37 @@ class ReaderBottomBar extends ConsumerWidget { ) { return Consumer( builder: (context, ref, child) { - final currentIndex = ref.watch(currentIndexProvider(chapter)); + return ValueListenableBuilder( + valueListenable: currentPageListenable, + builder: (context, currentIndex, child) { + final maxValue = (totalPages - 1).toDouble(); - final maxValue = (totalPages - 1).toDouble(); + final divisions = totalPages <= 1 ? null : totalPages - 1; - final divisions = totalPages <= 1 ? null : totalPages - 1; + final currentValue = min(currentIndex.toDouble(), maxValue); - final currentValue = min(currentIndex.toDouble(), maxValue); - - return SliderTheme( - data: SliderTheme.of(context).copyWith( - valueIndicatorShape: CustomValueIndicatorShape( - tranform: isReverseHorizontal, - ), - overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0), - ), - child: Slider( - onChanged: (value) { - onSliderChanged(value.toInt(), ref); - }, - onChangeEnd: (newValue) { - onSliderChangeEnd(newValue.toInt()); - }, - divisions: divisions, - value: currentValue, - label: currentIndexLabel(currentIndex), - min: 0, - max: maxValue, - ), + return SliderTheme( + data: SliderTheme.of(context).copyWith( + valueIndicatorShape: CustomValueIndicatorShape( + tranform: isReverseHorizontal, + ), + overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0), + ), + child: Slider( + onChanged: (value) { + onSliderChanged(value.toInt(), ref); + }, + onChangeEnd: (newValue) { + onSliderChangeEnd(newValue.toInt()); + }, + divisions: divisions, + value: currentValue, + label: currentIndexLabel(currentIndex), + min: 0, + max: maxValue, + ), + ); + }, ); }, ); diff --git a/lib/utils/extensions/manga.dart b/lib/utils/extensions/manga.dart index 6ad7bd46..ba6e5ad7 100644 --- a/lib/utils/extensions/manga.dart +++ b/lib/utils/extensions/manga.dart @@ -71,6 +71,8 @@ extension MangaExtensions on Manga { return filterDownloaded == 1 ? dl : !dl; }) .where((e) => !filterScanlator.contains(e.scanlator)) + .toList() + .reversed .toList(); }