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();