mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-24 00:12:16 +00:00
Reduce Code Duplication
This commit is contained in:
parent
63e747fa3e
commit
04e04010f4
8 changed files with 324 additions and 549 deletions
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
134
lib/modules/manga/reader/mixins/chapter_controller_mixin.dart
Normal file
134
lib/modules/manga/reader/mixins/chapter_controller_mixin.dart
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue