Reduce Code Duplication

This commit is contained in:
NBA2K1 2026-04-15 03:16:49 +02:00
parent 63e747fa3e
commit 04e04010f4
8 changed files with 324 additions and 549 deletions

View file

@ -279,7 +279,7 @@ class _AnimeStreamPageState extends riv.ConsumerState<AnimeStreamPage>
final ValueNotifier<double> _playbackSpeed = ValueNotifier(1.0);
final ValueNotifier<bool> _isDoubleSpeed = ValueNotifier(false);
late final ValueNotifier<Duration> _currentPosition = ValueNotifier(
_streamController.geTCurrentPosition(),
_streamController.getCurrentPosition(),
);
final ValueNotifier<Duration?> _currentTotalDuration = ValueNotifier(null);
final ValueNotifier<bool> _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,
);
}

View file

@ -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<bool>(() => 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()

View file

@ -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<Chapter> 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();
});
}
}

View file

@ -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<AutoScrollPages>? 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();
}
}

View file

@ -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<AutoScrollPages>? 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;

View file

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

View file

@ -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<AutoScrollPages>? 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!;
}
}

View file

@ -90,8 +90,8 @@ class _NovelWebViewState extends ConsumerState<NovelWebView>
_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();