mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-05-23 07:32:18 +00:00
Merge pull request #703 from NBA2K1/fix-reader
Fix reader and Code Cleanup
This commit is contained in:
commit
8595d62c71
25 changed files with 994 additions and 1171 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,11 +2,10 @@ 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/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/mixins/chapter_controller_mixin.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/modules/more/settings/player/providers/player_state_provider.dart';
|
||||
import 'package:mangayomi/services/aniskip.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
|
|
@ -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,35 @@ 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) 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 +61,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 +90,10 @@ class AnimeStreamController extends _$AnimeStreamController {
|
|||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// AniSkip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
(int, int)? _getTrackId() {
|
||||
final malId = isar.tracks
|
||||
.filter()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import 'package:mangayomi/modules/manga/home/manga_home_screen.dart';
|
|||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/router/router.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/services/search_.dart';
|
||||
import 'package:mangayomi/services/search.dart';
|
||||
import 'package:mangayomi/utils/cached_network.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
import 'package:mangayomi/utils/constant.dart';
|
||||
|
|
@ -156,7 +156,7 @@ class _SourceSearchScreenState extends ConsumerState<SourceSearchScreen> {
|
|||
source: widget.source,
|
||||
page: 1,
|
||||
query: widget.query,
|
||||
filterList: [],
|
||||
filterList: const [],
|
||||
).future,
|
||||
);
|
||||
if (mounted) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:mangayomi/models/chapter.dart';
|
|||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'library_state_provider.g.dart';
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import 'package:mangayomi/modules/library/providers/local_archive.dart';
|
|||
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/tracker_search_widget.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:mangayomi/modules/more/providers/algorithm_weights_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_pr
|
|||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/services/get_detail.dart';
|
||||
import 'package:mangayomi/services/search_.dart';
|
||||
import 'package:mangayomi/services/search.dart';
|
||||
import 'package:mangayomi/utils/cached_network.dart';
|
||||
import 'package:mangayomi/utils/date.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
|
|
@ -173,7 +173,7 @@ class _MigrationSourceSearchScreenState
|
|||
source: widget.source,
|
||||
page: 1,
|
||||
query: widget.query,
|
||||
filterList: [],
|
||||
filterList: const [],
|
||||
).future,
|
||||
);
|
||||
if (mounted) {
|
||||
|
|
|
|||
135
lib/modules/manga/reader/mixins/chapter_controller_mixin.dart
Normal file
135
lib/modules/manga/reader/mixins/chapter_controller_mixin.dart
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
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/utils/extensions/manga.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);
|
||||
Chapter getPrevChapter() => _chapterWithOffset(1);
|
||||
Chapter getNextChapter() => _chapterWithOffset(-1);
|
||||
|
||||
/// Finds this [chapter] in either the filtered list or the raw list and
|
||||
/// returns [index + offset]. The boolean indicates whether the filtered list
|
||||
/// was used (true) or the full 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.getChapterListForReading());
|
||||
if (index != null) return (index, true);
|
||||
// Fallback to raw list if chapter was filtered out.
|
||||
final all = manga.chapters.toList();
|
||||
return (findIn(all)!, false);
|
||||
}
|
||||
|
||||
Chapter _chapterWithOffset(int offset) {
|
||||
final idx = _chapterIndexWithOffset(offset);
|
||||
final list = idx.$2
|
||||
? getManga().getChapterListForReading()
|
||||
: getManga().chapters.toList();
|
||||
if (idx.$1 < 0 || idx.$1 >= list.length) {
|
||||
throw RangeError('No chapter at offset $offset from ${chapter.id}');
|
||||
}
|
||||
return list[idx.$1];
|
||||
}
|
||||
|
||||
int getChaptersLength(bool isInFilterList) => isInFilterList
|
||||
? getManga().getChapterListForReading().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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,12 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_riverpod/misc.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/download.dart';
|
||||
import 'package:mangayomi/models/history.dart';
|
||||
import 'package:mangayomi/models/manga.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/settings.dart';
|
||||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/models/track_preference.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
||||
import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/downloads/providers/downloads_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
import 'package:mangayomi/utils/extensions/chapter.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'reader_controller_provider.g.dart';
|
||||
|
|
@ -47,7 +39,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 +48,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 +76,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 +110,7 @@ class ReaderController extends _$ReaderController {
|
|||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
_invalidateSettingsCache();
|
||||
onSettingsMutated();
|
||||
}
|
||||
|
||||
void setPageMode(PageMode newPageMode) {
|
||||
|
|
@ -172,7 +132,7 @@ class ReaderController extends _$ReaderController {
|
|||
..updatedAt = DateTime.now().millisecondsSinceEpoch,
|
||||
),
|
||||
);
|
||||
_invalidateSettingsCache();
|
||||
onSettingsMutated();
|
||||
}
|
||||
|
||||
void setShowPageNumber(bool value) {
|
||||
|
|
@ -184,154 +144,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 +216,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,179 +225,4 @@ 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(
|
||||
updateProgressAfterReadingStateProvider,
|
||||
);
|
||||
if (!updateProgressAfterReading) return;
|
||||
final manga = this.manga.value!;
|
||||
final chapterNumber = ChapterRecognition().parseChapterNumber(
|
||||
manga.name!,
|
||||
name!,
|
||||
);
|
||||
|
||||
final tracks = isar.tracks
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.itemTypeEqualTo(manga.itemType)
|
||||
.mangaIdEqualTo(manga.id!)
|
||||
.findAllSync();
|
||||
|
||||
if (tracks.isEmpty) return;
|
||||
for (var track in tracks) {
|
||||
final service = isar.trackPreferences
|
||||
.filter()
|
||||
.syncIdIsNotNull()
|
||||
.syncIdEqualTo(track.syncId)
|
||||
.findFirstSync();
|
||||
if (!(service == null || chapterNumber <= (track.lastChapterRead ?? 0))) {
|
||||
if (track.status != TrackStatus.completed) {
|
||||
track.lastChapterRead = chapterNumber;
|
||||
if (track.lastChapterRead == track.totalChapter &&
|
||||
(track.totalChapter ?? 0) > 0) {
|
||||
track.status = TrackStatus.completed;
|
||||
track.finishedReadingDate = DateTime.now().millisecondsSinceEpoch;
|
||||
} else {
|
||||
track.status = manga.itemType == ItemType.manga
|
||||
? TrackStatus.reading
|
||||
: TrackStatus.watching;
|
||||
if (track.lastChapterRead == 1) {
|
||||
track.startedReadingDate = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
}
|
||||
ref
|
||||
.read(
|
||||
trackStateProvider(
|
||||
track: track,
|
||||
itemType: manga.itemType,
|
||||
widgetRef: ref,
|
||||
).notifier,
|
||||
)
|
||||
.updateManga();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension MangaExtensions on Manga {
|
||||
List<Chapter> getFilteredChapterList() {
|
||||
final data = this.chapters.toList().reversed.toList();
|
||||
final settings = isar.settings.getSync(227)!;
|
||||
final filterUnread =
|
||||
(settings.chapterFilterUnreadList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
ChapterFilterUnread(mangaId: id, type: 0))
|
||||
.type!;
|
||||
|
||||
final filterBookmarked =
|
||||
(settings.chapterFilterBookmarkedList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
ChapterFilterBookmarked(mangaId: id, type: 0))
|
||||
.type!;
|
||||
final filterDownloaded =
|
||||
(settings.chapterFilterDownloadedList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
ChapterFilterDownloaded(mangaId: id, type: 0))
|
||||
.type!;
|
||||
|
||||
final sortChapter =
|
||||
(settings.sortChapterList!
|
||||
.where((element) => element.mangaId == id)
|
||||
.toList()
|
||||
.firstOrNull ??
|
||||
SortChapter(mangaId: id, index: 1, reverse: false))
|
||||
.index;
|
||||
final filterScanlator = _getFilterScanlator(this) ?? [];
|
||||
final chapterIds = data.map((c) => c.id).whereType<int>().toList();
|
||||
final downloadedIds = (filterDownloaded == 0 || chapterIds.isEmpty)
|
||||
? const <int>{}
|
||||
: isar.downloads
|
||||
.filter()
|
||||
.anyOf(chapterIds, (q, id) => q.idEqualTo(id))
|
||||
.isDownloadEqualTo(true)
|
||||
.findAllSync()
|
||||
.map((d) => d.id!)
|
||||
.toSet();
|
||||
List<Chapter>? chapterList;
|
||||
chapterList = data
|
||||
.where(
|
||||
(element) => filterUnread == 1
|
||||
? element.isRead == false
|
||||
: filterUnread == 2
|
||||
? element.isRead == true
|
||||
: true,
|
||||
)
|
||||
.where(
|
||||
(element) => filterBookmarked == 1
|
||||
? element.isBookmarked == true
|
||||
: filterBookmarked == 2
|
||||
? element.isBookmarked == false
|
||||
: true,
|
||||
)
|
||||
.where((element) {
|
||||
if (filterDownloaded == 0) return true;
|
||||
final isDownloaded = downloadedIds.contains(element.id);
|
||||
return filterDownloaded == 1 ? isDownloaded : !isDownloaded;
|
||||
})
|
||||
.where((element) => !filterScanlator.contains(element.scanlator))
|
||||
.toList();
|
||||
List<Chapter> chapters = sortChapter == 0
|
||||
? chapterList.reversed.toList()
|
||||
: chapterList;
|
||||
if (sortChapter == 0) {
|
||||
chapters.sort((a, b) {
|
||||
return (a.scanlator == null ||
|
||||
b.scanlator == null ||
|
||||
a.dateUpload == null ||
|
||||
b.dateUpload == null)
|
||||
? 0
|
||||
: a.scanlator!.compareTo(b.scanlator!) |
|
||||
a.dateUpload!.compareTo(b.dateUpload!);
|
||||
});
|
||||
} else if (sortChapter == 2) {
|
||||
chapters.sort((a, b) {
|
||||
return (a.dateUpload == null || b.dateUpload == null)
|
||||
? 0
|
||||
: int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!));
|
||||
});
|
||||
} else if (sortChapter == 3) {
|
||||
chapters.sort((a, b) {
|
||||
return (a.name == null || b.name == null)
|
||||
? 0
|
||||
: a.name!.compareTo(b.name!);
|
||||
});
|
||||
}
|
||||
return chapters;
|
||||
}
|
||||
}
|
||||
|
||||
List<String>? _getFilterScanlator(Manga manga) {
|
||||
final scanlators = isar.settings.getSync(227)!.filterScanlatorList ?? [];
|
||||
final filter = scanlators
|
||||
.where((element) => element.mangaId == manga.id)
|
||||
.toList();
|
||||
return filter.firstOrNull?.scanlators;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import 'package:mangayomi/modules/manga/reader/widgets/page_indicator.dart';
|
|||
import 'package:mangayomi/modules/manga/reader/widgets/image_actions_dialog.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/extensions/others.dart';
|
||||
import 'package:mangayomi/utils/riverpod.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/services/get_chapter_pages.dart';
|
||||
|
|
@ -154,7 +155,6 @@ class _MangaChapterPageGalleryState
|
|||
bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows;
|
||||
final ValueNotifier<bool> _isScrolling = ValueNotifier(false);
|
||||
Timer? _scrollIdleTimer;
|
||||
bool _firstLaunch = true;
|
||||
final Stopwatch _readingStopwatch = Stopwatch();
|
||||
|
||||
/// Flag to prevent fullscreen from being disabled when navigating between
|
||||
|
|
@ -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();
|
||||
|
|
@ -176,6 +176,7 @@ class _MangaChapterPageGalleryState
|
|||
_autoScroll.value = false;
|
||||
_autoScroll.dispose();
|
||||
_autoScrollPage.dispose();
|
||||
_currentPageDisplayIndex.dispose();
|
||||
_scrollIdleTimer?.cancel();
|
||||
_isScrolling.dispose();
|
||||
_itemPositionsListener.itemPositions.removeListener(_readProgressListener);
|
||||
|
|
@ -194,11 +195,11 @@ class _MangaChapterPageGalleryState
|
|||
);
|
||||
}
|
||||
discordRpc?.showIdleText();
|
||||
final actualIdx = _pageViewToActualIndex(_currentIndex!);
|
||||
final actualIdx = _pageViewToActualIndexSync(_currentIndex!);
|
||||
final index = pages[actualIdx].index;
|
||||
if (index != null) {
|
||||
_readerController.setPageIndex(
|
||||
_isDoublePageActive ? index : _geCurrentIndex(index),
|
||||
_isDoublePageActiveSync ? index : _geCurrentIndex(index),
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
|
@ -237,6 +238,9 @@ class _MangaChapterPageGalleryState
|
|||
final _failedToLoadImage = ValueNotifier<bool>(false);
|
||||
|
||||
late int? _currentIndex = _readerController.getPageIndex();
|
||||
late final ValueNotifier<int> _currentPageDisplayIndex = ValueNotifier(
|
||||
_readerController.getPageIndex(),
|
||||
);
|
||||
|
||||
late final ItemScrollController _itemScrollController =
|
||||
ItemScrollController();
|
||||
|
|
@ -300,6 +304,9 @@ class _MangaChapterPageGalleryState
|
|||
final _currentReaderMode = StateProvider<ReaderMode?>(() => null);
|
||||
PageMode? _pageMode;
|
||||
bool _isView = false;
|
||||
|
||||
/// Cached reader mode to safely access in dispose without ref.read()
|
||||
ReaderMode? _cachedReaderMode;
|
||||
Alignment _scalePosition = Alignment.center;
|
||||
final PhotoViewController _photoViewController = PhotoViewController();
|
||||
final PhotoViewScaleStateController _photoViewScaleStateController =
|
||||
|
|
@ -346,6 +353,9 @@ class _MangaChapterPageGalleryState
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final animatePageTransitions = ref.watch(
|
||||
animatePageTransitionsStateProvider,
|
||||
);
|
||||
final backgroundColor = ref.watch(backgroundColorStateProvider);
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
final readerMode = ref.watch(_currentReaderMode);
|
||||
|
|
@ -358,13 +368,13 @@ class _MangaChapterPageGalleryState
|
|||
onPreviousPage: () => navigationService.previousPage(
|
||||
readerMode: readerMode!,
|
||||
currentIndex: _currentIndex!,
|
||||
animate: true,
|
||||
animate: animatePageTransitions,
|
||||
),
|
||||
onNextPage: () => navigationService.nextPage(
|
||||
readerMode: readerMode!,
|
||||
currentIndex: _currentIndex!,
|
||||
maxPages: _pageViewPageCount,
|
||||
animate: true,
|
||||
animate: animatePageTransitions,
|
||||
),
|
||||
onEscape: () => _goBack(context),
|
||||
onFullScreen: () => _setFullScreen(),
|
||||
|
|
@ -422,8 +432,9 @@ class _MangaChapterPageGalleryState
|
|||
scrollDirection: isHorizontalContinuous
|
||||
? Axis.horizontal
|
||||
: Axis.vertical,
|
||||
minCacheExtent:
|
||||
pagePreloadAmount * context.height(1),
|
||||
minCacheExtent: isHorizontalContinuous
|
||||
? pagePreloadAmount * context.width(1)
|
||||
: pagePreloadAmount * context.height(1),
|
||||
initialScrollIndex: _readerController
|
||||
.getPageIndex(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
|
|
@ -758,13 +769,13 @@ class _MangaChapterPageGalleryState
|
|||
onPreviousPage: () => navigationService.previousPage(
|
||||
readerMode: readerMode!,
|
||||
currentIndex: _currentIndex!,
|
||||
animate: true,
|
||||
animate: animatePageTransitions,
|
||||
),
|
||||
onNextPage: () => navigationService.nextPage(
|
||||
readerMode: readerMode!,
|
||||
currentIndex: _currentIndex!,
|
||||
maxPages: _pageViewPageCount,
|
||||
animate: true,
|
||||
animate: animatePageTransitions,
|
||||
),
|
||||
onDoubleTapDown: (position) => _toggleScale(position),
|
||||
onDoubleTap: () {},
|
||||
|
|
@ -824,6 +835,7 @@ class _MangaChapterPageGalleryState
|
|||
);
|
||||
},
|
||||
onSliderChanged: (value, ref) {
|
||||
_currentPageDisplayIndex.value = value;
|
||||
ref
|
||||
.read(currentIndexProvider(chapter).notifier)
|
||||
.setCurrentIndex(value);
|
||||
|
|
@ -907,7 +919,7 @@ class _MangaChapterPageGalleryState
|
|||
},
|
||||
),
|
||||
currentReaderModeProvider: _currentReaderMode,
|
||||
currentIndexProvider: currentIndexProvider,
|
||||
currentPageListenable: _currentPageDisplayIndex,
|
||||
currentPageMode: _pageMode,
|
||||
isReverseHorizontal: _isReverseHorizontal,
|
||||
totalPages: _readerController.getPageLength(
|
||||
|
|
@ -917,8 +929,8 @@ class _MangaChapterPageGalleryState
|
|||
backgroundColor: _backgroundColor,
|
||||
),
|
||||
PageIndicator(
|
||||
chapter: chapter,
|
||||
isUiVisible: _isView,
|
||||
currentPageListenable: _currentPageDisplayIndex,
|
||||
totalPages: _readerController.getPageLength(
|
||||
_chapterUrlModel.pageUrls,
|
||||
),
|
||||
|
|
@ -1008,6 +1020,7 @@ class _MangaChapterPageGalleryState
|
|||
|
||||
final idx = pages[_currentIndex!].index;
|
||||
if (idx != null) {
|
||||
_currentPageDisplayIndex.value = idx;
|
||||
_readerController.setPageIndex(
|
||||
_isDoublePageActive ? idx : _geCurrentIndex(idx),
|
||||
false,
|
||||
|
|
@ -1061,7 +1074,10 @@ class _MangaChapterPageGalleryState
|
|||
if (_isNextChapterPreloading || _isLastPageTransition) return;
|
||||
_isNextChapterPreloading = true;
|
||||
try {
|
||||
if (!mounted) return;
|
||||
if (!mounted) {
|
||||
_isNextChapterPreloading = false;
|
||||
return;
|
||||
}
|
||||
final nextChapter = _readerController.getNextChapter();
|
||||
if (isChapterLoaded(nextChapter)) {
|
||||
_isNextChapterPreloading = false;
|
||||
|
|
@ -1155,6 +1171,7 @@ class _MangaChapterPageGalleryState
|
|||
void _initCurrentIndex() async {
|
||||
if (ref.read(cropBordersStateProvider)) _processCropBorders();
|
||||
final readerMode = _readerController.getReaderMode();
|
||||
_currentPageDisplayIndex.value = _readerController.getPageIndex();
|
||||
|
||||
// Initialize the preload manager with bounded memory (from ReaderMemoryManagement mixin)
|
||||
initializePreloadManager(
|
||||
|
|
@ -1167,10 +1184,14 @@ class _MangaChapterPageGalleryState
|
|||
},
|
||||
);
|
||||
|
||||
// Kick off ordered prefetch before the first frame so lower-indexed pages
|
||||
// win the HTTP race against the simultaneous widget-driven loads.
|
||||
_prefetchPagesInOrder(); // intentionally not awaited
|
||||
|
||||
// proactively start loading adjacent chapters in background
|
||||
_proactivePreload();
|
||||
|
||||
_readerController.setMangaHistoryUpdate();
|
||||
_readerController.setHistoryUpdate();
|
||||
// Use post-frame callback instead of Future.delayed(1ms) timing hack
|
||||
await Future(() {});
|
||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||
|
|
@ -1202,22 +1223,55 @@ class _MangaChapterPageGalleryState
|
|||
}
|
||||
}
|
||||
|
||||
/// Warms Flutter's [ImageCache] in page order before the widget tree renders.
|
||||
///
|
||||
/// [ScrollablePositionedList] builds all items within [minCacheExtent] in a
|
||||
/// single frame, firing every network request simultaneously, which means
|
||||
/// pages complete in arbitrary (server-response) order. By resolving each
|
||||
/// provider sequentially here — starting before that first frame — we seed
|
||||
/// the cache so that earlier pages win the HTTP race: lower-indexed pages
|
||||
/// start their requests first and are therefore ready sooner.
|
||||
///
|
||||
/// For pages already within the cache extent the widget will attach to the
|
||||
/// already-pending Future (Flutter deduplicates by provider key), so no
|
||||
/// extra requests are made. Pages beyond the cache extent are fetched
|
||||
/// strictly one at a time in reading order, so the reader never sees a
|
||||
/// later page appear before an earlier one.
|
||||
///
|
||||
/// This is fully async — [await] inside a fire-and-forget call — so the
|
||||
/// UI stays interactive throughout.
|
||||
Future<void> _prefetchPagesInOrder() async {
|
||||
final startIdx = (_currentIndex ?? 0).clamp(0, pages.length - 1);
|
||||
|
||||
// Visit pages from the opening position forward, then backward.
|
||||
final indices = [
|
||||
for (var i = startIdx; i < pages.length; i++) i,
|
||||
for (var i = startIdx - 1; i >= 0; i--) i,
|
||||
];
|
||||
|
||||
for (final i in indices) {
|
||||
if (!mounted) return;
|
||||
final page = pages[i];
|
||||
if (page.isTransitionPage) continue;
|
||||
try {
|
||||
// Awaiting ensures page[i] finishes (or fails) before page[i+1]
|
||||
// starts downloading, giving strict reading-order priority.
|
||||
await precacheImage(page.getImageProvider(ref, true), context);
|
||||
} catch (_) {
|
||||
// Swallow errors: network failures, widget disposal, etc.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onPageChanged(int index) async {
|
||||
// In non-continuous double page mode, convert page view index to actual
|
||||
// pages array index for correct lookups.
|
||||
final int actualIndex = _pageViewToActualIndex(index);
|
||||
final int prevActualIndex = _pageViewToActualIndex(_currentIndex!);
|
||||
|
||||
final cropBorders = ref.watch(cropBordersStateProvider);
|
||||
if (cropBorders) {
|
||||
_processCropBordersByIndex(index);
|
||||
}
|
||||
if (_firstLaunch) {
|
||||
Future.delayed(const Duration(milliseconds: 100)).then((_) {
|
||||
_firstLaunch = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
final idx = pages[prevActualIndex].index;
|
||||
if (idx != null) {
|
||||
_readerController.setPageIndex(
|
||||
|
|
@ -1246,6 +1300,7 @@ class _MangaChapterPageGalleryState
|
|||
clearGestureDetailsCache();
|
||||
_currentIndex = index;
|
||||
if (pages[actualIndex].index != null) {
|
||||
_currentPageDisplayIndex.value = pages[actualIndex].index!;
|
||||
ref
|
||||
.read(currentIndexProvider(chapter).notifier)
|
||||
.setCurrentIndex(pages[actualIndex].index!);
|
||||
|
|
@ -1324,6 +1379,9 @@ class _MangaChapterPageGalleryState
|
|||
_failedToLoadImage.value = false;
|
||||
_readerController.setReaderMode(value);
|
||||
|
||||
// Cache the reader mode for safe access in dispose
|
||||
_cachedReaderMode = value;
|
||||
|
||||
int index = _pageViewToActualIndex(_currentIndex!);
|
||||
ref.read(_currentReaderMode.notifier).state = value;
|
||||
if (value == ReaderMode.vertical) {
|
||||
|
|
@ -1451,11 +1509,19 @@ class _MangaChapterPageGalleryState
|
|||
|
||||
/// Whether double page mode is active (continuous or paged).
|
||||
/// Horizontal continuous mode does NOT use double page layout.
|
||||
/// Uses ref.read() so cannot be called during dispose.
|
||||
bool get _isDoublePageActive =>
|
||||
_pageMode == PageMode.doublePage &&
|
||||
ref.read(_currentReaderMode) != ReaderMode.horizontalContinuous &&
|
||||
ref.read(_currentReaderMode) != ReaderMode.horizontalContinuousRTL;
|
||||
|
||||
/// Safe version of _isDoublePageActive that uses cached reader mode.
|
||||
/// Safe to call during dispose without Riverpod assertion errors.
|
||||
bool get _isDoublePageActiveSync =>
|
||||
_pageMode == PageMode.doublePage &&
|
||||
_cachedReaderMode != ReaderMode.horizontalContinuous &&
|
||||
_cachedReaderMode != ReaderMode.horizontalContinuousRTL;
|
||||
|
||||
/// Converts a page view index (from ExtendedPageController) to the actual
|
||||
/// index in the [pages] array for double page mode.
|
||||
///
|
||||
|
|
@ -1467,6 +1533,12 @@ class _MangaChapterPageGalleryState
|
|||
return (pageViewIndex * 2).clamp(0, pages.length - 1);
|
||||
}
|
||||
|
||||
/// Safe version that uses cached reader mode for use in dispose.
|
||||
int _pageViewToActualIndexSync(int pageViewIndex) {
|
||||
if (!_isDoublePageActiveSync) return pageViewIndex;
|
||||
return (pageViewIndex * 2).clamp(0, pages.length - 1);
|
||||
}
|
||||
|
||||
/// Converts an actual [pages] array index to a page view index
|
||||
/// for double page mode.
|
||||
int _actualToPageViewIndex(int actualIndex) {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import 'package:mangayomi/main.dart';
|
|||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
import 'package:mangayomi/utils/date.dart';
|
||||
|
|
|
|||
|
|
@ -1,216 +1,323 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||
|
||||
class ChapterTransitionPage extends StatelessWidget {
|
||||
final Chapter currentChapter;
|
||||
final Chapter? nextChapter;
|
||||
final String mangaName;
|
||||
final ReaderMode readerMode;
|
||||
|
||||
const ChapterTransitionPage({
|
||||
super.key,
|
||||
required this.currentChapter,
|
||||
required this.nextChapter,
|
||||
required this.mangaName,
|
||||
required this.readerMode,
|
||||
});
|
||||
|
||||
bool get _isVertical =>
|
||||
readerMode == ReaderMode.vertical ||
|
||||
readerMode == ReaderMode.verticalContinuous ||
|
||||
readerMode == ReaderMode.webtoon;
|
||||
|
||||
bool get _isRTL =>
|
||||
readerMode == ReaderMode.rtl ||
|
||||
readerMode == ReaderMode.horizontalContinuousRTL;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final l10n = context.l10n;
|
||||
return Container(
|
||||
constraints: BoxConstraints(minHeight: screenHeight * 0.5),
|
||||
color: Theme.of(context).scaffoldBackgroundColor,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(screenWidth * 0.08),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Icône de transition
|
||||
Icon(
|
||||
Icons.auto_stories_outlined,
|
||||
size: screenWidth * 0.16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
|
||||
SizedBox(height: screenHeight * 0.04),
|
||||
|
||||
// Titre
|
||||
Text(
|
||||
l10n.end_of_chapter,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
||||
SizedBox(height: screenHeight * 0.03),
|
||||
|
||||
Container(
|
||||
padding: EdgeInsets.all(screenWidth * 0.04),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withValues(alpha: 0.2),
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: ConstrainedBox(
|
||||
// Give the content a natural maximum size to fit within.
|
||||
// FittedBox will scale it down if the available space is smaller.
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _isVertical
|
||||
? constraints.maxWidth.clamp(100.0, 480.0)
|
||||
: constraints.maxWidth.clamp(100.0, double.infinity),
|
||||
maxHeight: constraints.maxHeight.clamp(
|
||||
100.0,
|
||||
double.infinity,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.chapter_completed,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
SizedBox(height: screenHeight * 0.01),
|
||||
Text(
|
||||
currentChapter.name ?? 'Chapitre ${currentChapter.id}',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: _isVertical
|
||||
? _buildVerticalLayout(context)
|
||||
: _buildHorizontalLayout(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SizedBox(height: screenHeight * 0.03),
|
||||
// ── Vertical layout (top → arrow → bottom) ────────────────────────────────
|
||||
|
||||
Icon(
|
||||
nextChapter != null
|
||||
? Icons.keyboard_arrow_down
|
||||
: Icons.check_circle_outline,
|
||||
size: screenWidth * 0.08,
|
||||
color: nextChapter != null
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
|
||||
SizedBox(height: screenHeight * 0.03),
|
||||
|
||||
if (nextChapter != null) ...[
|
||||
Container(
|
||||
padding: EdgeInsets.all(screenWidth * 0.04),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primary.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
l10n.next_chapter,
|
||||
style: Theme.of(context).textTheme.labelMedium
|
||||
?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onPrimaryContainer
|
||||
.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
SizedBox(height: screenHeight * 0.01),
|
||||
Text(
|
||||
nextChapter!.name ?? 'Chapitre ${nextChapter!.id}',
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: screenHeight * 0.04),
|
||||
Text(
|
||||
l10n.continue_to_next_chapter,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
] else ...[
|
||||
Container(
|
||||
padding: EdgeInsets.all(screenWidth * 0.04),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.last_page,
|
||||
size: screenWidth * 0.06,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
SizedBox(height: screenHeight * 0.01),
|
||||
Text(
|
||||
l10n.no_next_chapter,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: screenHeight * 0.005),
|
||||
Text(
|
||||
l10n.you_have_finished_reading,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: screenHeight * 0.04),
|
||||
Text(
|
||||
l10n.return_to_the_list_of_chapters,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
Widget _buildVerticalLayout(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return IntrinsicWidth(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_stories_outlined,
|
||||
size: 48,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.end_of_chapter,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildChapterCard(
|
||||
context,
|
||||
label: l10n.chapter_completed,
|
||||
name: currentChapter.name ?? 'Chapter ${currentChapter.id}',
|
||||
isPrimary: false,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Icon(
|
||||
nextChapter != null
|
||||
? Icons.keyboard_arrow_down
|
||||
: Icons.check_circle_outline,
|
||||
size: 32,
|
||||
color: nextChapter != null
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (nextChapter != null) ...[
|
||||
_buildChapterCard(
|
||||
context,
|
||||
label: l10n.next_chapter,
|
||||
name: nextChapter!.name ?? 'Chapter ${nextChapter!.id}',
|
||||
isPrimary: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.continue_to_next_chapter,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
] else ...[
|
||||
_buildEndOfMangaCard(context),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
l10n.return_to_the_list_of_chapters,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ── Horizontal layout (left → arrow → right) ──────────────────────────────
|
||||
|
||||
Widget _buildHorizontalLayout(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
// For LTR: [current] → [next]
|
||||
// For RTL: [next] ← [current]
|
||||
final Widget currentCard = _buildChapterCard(
|
||||
context,
|
||||
label: l10n.chapter_completed,
|
||||
name: currentChapter.name ?? 'Chapter ${currentChapter.id}',
|
||||
isPrimary: false,
|
||||
);
|
||||
|
||||
final Widget arrowIcon = Icon(
|
||||
nextChapter != null
|
||||
? (_isRTL ? Icons.keyboard_arrow_left : Icons.keyboard_arrow_right)
|
||||
: Icons.check_circle_outline,
|
||||
size: 36,
|
||||
color: nextChapter != null
|
||||
? theme.colorScheme.primary
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
);
|
||||
|
||||
final Widget nextCard = nextChapter != null
|
||||
? _buildChapterCard(
|
||||
context,
|
||||
label: l10n.next_chapter,
|
||||
name: nextChapter!.name ?? 'Chapter ${nextChapter!.id}',
|
||||
isPrimary: true,
|
||||
)
|
||||
: _buildEndOfMangaCard(context);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.auto_stories_outlined,
|
||||
size: 40,
|
||||
color: theme.colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.end_of_chapter,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
IntrinsicHeight(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: _isRTL
|
||||
? [
|
||||
Expanded(child: nextCard),
|
||||
const SizedBox(width: 12),
|
||||
Center(child: arrowIcon),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: currentCard),
|
||||
]
|
||||
: [
|
||||
Expanded(child: currentCard),
|
||||
const SizedBox(width: 12),
|
||||
Center(child: arrowIcon),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: nextCard),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
nextChapter != null
|
||||
? l10n.continue_to_next_chapter
|
||||
: l10n.return_to_the_list_of_chapters,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ── Shared card widgets ────────────────────────────────────────────────────
|
||||
|
||||
Widget _buildChapterCard(
|
||||
BuildContext context, {
|
||||
required String label,
|
||||
required String name,
|
||||
required bool isPrimary,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
final bgColor = isPrimary
|
||||
? theme.colorScheme.primaryContainer
|
||||
: theme.colorScheme.surface;
|
||||
final borderColor = isPrimary
|
||||
? theme.colorScheme.primary.withValues(alpha: 0.3)
|
||||
: theme.colorScheme.outline.withValues(alpha: 0.2);
|
||||
final labelColor = isPrimary
|
||||
? theme.colorScheme.onPrimaryContainer.withValues(alpha: 0.8)
|
||||
: theme.colorScheme.onSurface.withValues(alpha: 0.7);
|
||||
final nameColor = isPrimary ? theme.colorScheme.onPrimaryContainer : null;
|
||||
|
||||
return SizedBox(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: theme.textTheme.labelMedium?.copyWith(color: labelColor),
|
||||
maxLines: 2,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
name,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: nameColor,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEndOfMangaCard(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final theme = Theme.of(context);
|
||||
return SizedBox(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.3),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.last_page,
|
||||
size: 24,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
l10n.no_next_chapter,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.8),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.you_have_finished_reading,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
|
||||
|
||||
/// Page indicator widget showing current page / total pages.
|
||||
|
|
@ -9,12 +8,12 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr
|
|||
/// Displayed at the bottom center when the UI is hidden and
|
||||
/// "show page numbers" setting is enabled.
|
||||
class PageIndicator extends ConsumerWidget {
|
||||
/// The current chapter being read.
|
||||
final Chapter chapter;
|
||||
|
||||
/// Whether the UI overlay is currently visible.
|
||||
final bool isUiVisible;
|
||||
|
||||
/// Session-local current page index for the visible reader state.
|
||||
final ValueListenable<int> currentPageListenable;
|
||||
|
||||
/// Total number of pages.
|
||||
final int totalPages;
|
||||
|
||||
|
|
@ -23,15 +22,14 @@ class PageIndicator extends ConsumerWidget {
|
|||
|
||||
const PageIndicator({
|
||||
super.key,
|
||||
required this.chapter,
|
||||
required this.isUiVisible,
|
||||
required this.currentPageListenable,
|
||||
required this.totalPages,
|
||||
required this.formatCurrentIndex,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentIndex = ref.watch(currentIndexProvider(chapter));
|
||||
final showPagesNumber = ref.watch(showPagesNumberStateProvider);
|
||||
|
||||
// Don't show when UI is visible or setting is disabled
|
||||
|
|
@ -41,19 +39,24 @@ class PageIndicator extends ConsumerWidget {
|
|||
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Text(
|
||||
'${formatCurrentIndex(currentIndex)} / $totalPages',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.0,
|
||||
shadows: [
|
||||
Shadow(offset: Offset(-1, -1), blurRadius: 1),
|
||||
Shadow(offset: Offset(1, -1), blurRadius: 1),
|
||||
Shadow(offset: Offset(1, 1), blurRadius: 1),
|
||||
Shadow(offset: Offset(-1, 1), blurRadius: 1),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
child: ValueListenableBuilder<int>(
|
||||
valueListenable: currentPageListenable,
|
||||
builder: (context, currentIndex, child) {
|
||||
return Text(
|
||||
'${formatCurrentIndex(currentIndex)} / $totalPages',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20.0,
|
||||
shadows: [
|
||||
Shadow(offset: Offset(-1, -1), blurRadius: 1),
|
||||
Shadow(offset: Offset(1, -1), blurRadius: 1),
|
||||
Shadow(offset: Offset(1, 1), blurRadius: 1),
|
||||
Shadow(offset: Offset(-1, 1), blurRadius: 1),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
|
@ -6,7 +7,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:flutter_riverpod/misc.dart' show ProviderListenable;
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/custom_value_indicator_shape.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
|
||||
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
|
||||
|
|
@ -62,9 +62,8 @@ class ReaderBottomBar extends ConsumerWidget {
|
|||
/// (StateProvider, NotifierProvider, etc.)
|
||||
final ProviderListenable<ReaderMode?> currentReaderModeProvider;
|
||||
|
||||
/// Provider family for watching current page index
|
||||
/// Type: CurrentIndexFamily (from reader_controller_provider.g.dart)
|
||||
final CurrentIndexFamily currentIndexProvider;
|
||||
/// Session-local current page index for the visible reader state.
|
||||
final ValueListenable<int> currentPageListenable;
|
||||
|
||||
/// Current page mode (nullable for safety)
|
||||
final PageMode? currentPageMode;
|
||||
|
|
@ -95,7 +94,7 @@ class ReaderBottomBar extends ConsumerWidget {
|
|||
this.onPageModeToggle,
|
||||
required this.onSettingsPressed,
|
||||
required this.currentReaderModeProvider,
|
||||
required this.currentIndexProvider,
|
||||
required this.currentPageListenable,
|
||||
required this.currentPageMode,
|
||||
required this.isReverseHorizontal,
|
||||
required this.totalPages,
|
||||
|
|
@ -194,15 +193,17 @@ class ReaderBottomBar extends ConsumerWidget {
|
|||
child: Center(
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final currentIndex = ref.watch(
|
||||
currentIndexProvider(chapter),
|
||||
);
|
||||
return Text(
|
||||
currentIndexLabel(currentIndex),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: currentPageListenable,
|
||||
builder: (context, currentIndex, child) {
|
||||
return Text(
|
||||
currentIndexLabel(currentIndex),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
|
@ -277,34 +278,37 @@ class ReaderBottomBar extends ConsumerWidget {
|
|||
) {
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final currentIndex = ref.watch(currentIndexProvider(chapter));
|
||||
return ValueListenableBuilder<int>(
|
||||
valueListenable: currentPageListenable,
|
||||
builder: (context, currentIndex, child) {
|
||||
final maxValue = (totalPages - 1).toDouble();
|
||||
|
||||
final maxValue = (totalPages - 1).toDouble();
|
||||
final divisions = totalPages <= 1 ? null : totalPages - 1;
|
||||
|
||||
final divisions = totalPages <= 1 ? null : totalPages - 1;
|
||||
final currentValue = min(currentIndex.toDouble(), maxValue);
|
||||
|
||||
final currentValue = min(currentIndex.toDouble(), maxValue);
|
||||
|
||||
return SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
valueIndicatorShape: CustomValueIndicatorShape(
|
||||
tranform: isReverseHorizontal,
|
||||
),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||
),
|
||||
child: Slider(
|
||||
onChanged: (value) {
|
||||
onSliderChanged(value.toInt(), ref);
|
||||
},
|
||||
onChangeEnd: (newValue) {
|
||||
onSliderChangeEnd(newValue.toInt());
|
||||
},
|
||||
divisions: divisions,
|
||||
value: currentValue,
|
||||
label: currentIndexLabel(currentIndex),
|
||||
min: 0,
|
||||
max: maxValue,
|
||||
),
|
||||
return SliderTheme(
|
||||
data: SliderTheme.of(context).copyWith(
|
||||
valueIndicatorShape: CustomValueIndicatorShape(
|
||||
tranform: isReverseHorizontal,
|
||||
),
|
||||
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||
),
|
||||
child: Slider(
|
||||
onChanged: (value) {
|
||||
onSliderChanged(value.toInt(), ref);
|
||||
},
|
||||
onChangeEnd: (newValue) {
|
||||
onSliderChangeEnd(newValue.toInt());
|
||||
},
|
||||
divisions: divisions,
|
||||
value: currentValue,
|
||||
label: currentIndexLabel(currentIndex),
|
||||
min: 0,
|
||||
max: maxValue,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/chapter_transition_page.dart';
|
||||
|
||||
|
|
@ -14,10 +15,15 @@ class TransitionViewPaged extends ConsumerWidget {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final readerMode = ref
|
||||
.read(readerControllerProvider(chapter: data.chapter!).notifier)
|
||||
.getReaderMode();
|
||||
|
||||
return ChapterTransitionPage(
|
||||
currentChapter: data.chapter!,
|
||||
nextChapter: data.nextChapter,
|
||||
mangaName: data.mangaName ?? '',
|
||||
readerMode: readerMode,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/widgets/chapter_transition_page.dart';
|
||||
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||
|
|
@ -15,12 +16,17 @@ class TransitionViewVertical extends ConsumerWidget {
|
|||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final readerMode = ref
|
||||
.read(readerControllerProvider(chapter: data.chapter!).notifier)
|
||||
.getReaderMode();
|
||||
|
||||
return SizedBox(
|
||||
height: context.height(1),
|
||||
child: ChapterTransitionPage(
|
||||
currentChapter: data.chapter!,
|
||||
nextChapter: data.nextChapter,
|
||||
mangaName: data.mangaName ?? '',
|
||||
readerMode: readerMode,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
|
|
@ -15,7 +13,7 @@ import 'package:mangayomi/modules/mass_migration/models/mass_migration_models.da
|
|||
import 'package:mangayomi/modules/manga/detail/providers/isar_providers.dart';
|
||||
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
|
||||
import 'package:mangayomi/services/get_detail.dart';
|
||||
import 'package:mangayomi/services/search_.dart';
|
||||
import 'package:mangayomi/services/search.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
|
||||
Future<void> migrateLibraryItem({
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ Future<void> checkForUpdate(
|
|||
bool? manualUpdate,
|
||||
}) async {
|
||||
manualUpdate = manualUpdate ?? false;
|
||||
final checkForUpdates = ref.watch(checkForAppUpdatesProvider);
|
||||
final checkForUpdates = ref.read(checkForAppUpdatesProvider);
|
||||
if (!checkForUpdates && !manualUpdate) return;
|
||||
final l10n = l10nLocalizations(context!)!;
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ Future<GetChapterPagesModel> getChapterPages(
|
|||
}) async {
|
||||
final keepAlive = ref.keepAlive();
|
||||
try {
|
||||
List<UChapDataPreload> uChapDataPreloadp = [];
|
||||
Directory? path;
|
||||
List<PageUrl> pageUrls = [];
|
||||
List<bool> isLocaleList = [];
|
||||
|
|
@ -86,6 +85,14 @@ Future<GetChapterPagesModel> getChapterPages(
|
|||
}
|
||||
}
|
||||
|
||||
final chapterModel = GetChapterPagesModel(
|
||||
path: path,
|
||||
pageUrls: pageUrls,
|
||||
isLocaleList: isLocaleList,
|
||||
archiveImages: archiveImages,
|
||||
uChapDataPreload: [],
|
||||
);
|
||||
|
||||
if (pageUrls.isNotEmpty || isLocalArchive) {
|
||||
if (await File(
|
||||
p.join(mangaDirectory!.path, "${chapter.name}.cbz"),
|
||||
|
|
@ -144,7 +151,7 @@ Future<GetChapterPagesModel> getChapterPages(
|
|||
});
|
||||
}
|
||||
for (var i = 0; i < pageUrls.length; i++) {
|
||||
uChapDataPreloadp.add(
|
||||
chapterModel.uChapDataPreload.add(
|
||||
UChapDataPreload(
|
||||
chapter,
|
||||
path,
|
||||
|
|
@ -152,26 +159,14 @@ Future<GetChapterPagesModel> getChapterPages(
|
|||
isLocaleList[i],
|
||||
archiveImages[i],
|
||||
i,
|
||||
GetChapterPagesModel(
|
||||
path: path,
|
||||
pageUrls: pageUrls,
|
||||
isLocaleList: isLocaleList,
|
||||
archiveImages: archiveImages,
|
||||
uChapDataPreload: uChapDataPreloadp,
|
||||
),
|
||||
chapterModel,
|
||||
i,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
keepAlive.close();
|
||||
return GetChapterPagesModel(
|
||||
path: path,
|
||||
pageUrls: pageUrls,
|
||||
isLocaleList: isLocaleList,
|
||||
archiveImages: archiveImages,
|
||||
uChapDataPreload: uChapDataPreloadp,
|
||||
);
|
||||
return chapterModel;
|
||||
} catch (e) {
|
||||
keepAlive.close();
|
||||
rethrow;
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
import 'package:isar_community/isar.dart';
|
||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/source.dart';
|
||||
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
|
||||
import 'package:mangayomi/services/isolate_service.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
part 'search_.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<MPages?> search(
|
||||
Ref ref, {
|
||||
required Source source,
|
||||
required String query,
|
||||
required int page,
|
||||
required List<dynamic> filterList,
|
||||
}) async {
|
||||
if (source.name == "local" && source.lang == "") {
|
||||
final result =
|
||||
(await isar.mangas
|
||||
.filter()
|
||||
.group(
|
||||
(q) => q
|
||||
.sourceEqualTo("local")
|
||||
.or()
|
||||
.linkContains("Mangayomi/local")
|
||||
.or()
|
||||
.linkContains("Mangayomi\\local"),
|
||||
)
|
||||
.nameContains(query, caseSensitive: false)
|
||||
.offset(page * 50)
|
||||
.limit(50)
|
||||
.findAll())
|
||||
.map((e) => MManga(name: e.name))
|
||||
.toList();
|
||||
return MPages(list: result, hasNextPage: true);
|
||||
}
|
||||
return getIsolateService.get<MPages?>(
|
||||
query: query,
|
||||
filterList: filterList,
|
||||
source: source,
|
||||
page: page,
|
||||
serviceType: 'search',
|
||||
proxyServer: ref.read(androidProxyServerStateProvider),
|
||||
);
|
||||
}
|
||||
|
|
@ -1,109 +0,0 @@
|
|||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'search_.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(search)
|
||||
final searchProvider = SearchFamily._();
|
||||
|
||||
final class SearchProvider
|
||||
extends $FunctionalProvider<AsyncValue<MPages?>, MPages?, FutureOr<MPages?>>
|
||||
with $FutureModifier<MPages?>, $FutureProvider<MPages?> {
|
||||
SearchProvider._({
|
||||
required SearchFamily super.from,
|
||||
required ({Source source, String query, int page, List<dynamic> filterList})
|
||||
super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'searchProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$searchHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'searchProvider'
|
||||
''
|
||||
'$argument';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<MPages?> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<MPages?> create(Ref ref) {
|
||||
final argument =
|
||||
this.argument
|
||||
as ({
|
||||
Source source,
|
||||
String query,
|
||||
int page,
|
||||
List<dynamic> filterList,
|
||||
});
|
||||
return search(
|
||||
ref,
|
||||
source: argument.source,
|
||||
query: argument.query,
|
||||
page: argument.page,
|
||||
filterList: argument.filterList,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SearchProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$searchHash() => r'0fa9d882436b1b58b3420dae5a757e7622273eb5';
|
||||
|
||||
final class SearchFamily extends $Family
|
||||
with
|
||||
$FunctionalFamilyOverride<
|
||||
FutureOr<MPages?>,
|
||||
({Source source, String query, int page, List<dynamic> filterList})
|
||||
> {
|
||||
SearchFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'searchProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
SearchProvider call({
|
||||
required Source source,
|
||||
required String query,
|
||||
required int page,
|
||||
required List<dynamic> filterList,
|
||||
}) => SearchProvider._(
|
||||
argument: (
|
||||
source: source,
|
||||
query: query,
|
||||
page: page,
|
||||
filterList: filterList,
|
||||
),
|
||||
from: this,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() => r'searchProvider';
|
||||
}
|
||||
|
|
@ -1,13 +1,21 @@
|
|||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/download.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/track.dart';
|
||||
import 'package:mangayomi/models/track_preference.dart';
|
||||
import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/push_router.dart';
|
||||
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
|
||||
import 'package:mangayomi/utils/extensions/manga.dart';
|
||||
import 'package:mangayomi/modules/more/settings/track/providers/track_providers.dart';
|
||||
import 'package:mangayomi/providers/storage_provider.dart';
|
||||
import 'package:mangayomi/services/download_manager/download_isolate_pool.dart';
|
||||
import 'package:mangayomi/services/download_manager/m_downloader.dart';
|
||||
import 'package:mangayomi/utils/chapter_recognition.dart';
|
||||
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
|
|
@ -19,9 +27,9 @@ extension ChapterExtension on Chapter {
|
|||
if (ignoreIsRead || !isRead!) {
|
||||
await pushMangaReaderView(context: context, chapter: this);
|
||||
} else {
|
||||
final filteredChaps = manga.value!.getFilteredChapterList();
|
||||
final filteredChaps = manga.value!.getChapterListForReading();
|
||||
bool exist = false;
|
||||
for (var filteredChap in filteredChaps.reversed) {
|
||||
for (var filteredChap in filteredChaps) {
|
||||
if (filteredChap.toJson().toString() == toJson().toString()) {
|
||||
exist = true;
|
||||
}
|
||||
|
|
@ -80,4 +88,59 @@ extension ChapterExtension on Chapter {
|
|||
|
||||
cancelDownloads(download.id);
|
||||
}
|
||||
|
||||
void updateTrackChapterRead(dynamic ref) {
|
||||
if (!(ref is WidgetRef || ref is Ref)) return;
|
||||
final updateProgressAfterReading = ref.read(
|
||||
updateProgressAfterReadingStateProvider,
|
||||
);
|
||||
if (!updateProgressAfterReading) return;
|
||||
final manga = this.manga.value!;
|
||||
final chapterNumber = ChapterRecognition().parseChapterNumber(
|
||||
manga.name!,
|
||||
name!,
|
||||
);
|
||||
|
||||
final tracks = isar.tracks
|
||||
.filter()
|
||||
.idIsNotNull()
|
||||
.itemTypeEqualTo(manga.itemType)
|
||||
.mangaIdEqualTo(manga.id!)
|
||||
.findAllSync();
|
||||
|
||||
if (tracks.isEmpty) return;
|
||||
for (var track in tracks) {
|
||||
final service = isar.trackPreferences
|
||||
.filter()
|
||||
.syncIdIsNotNull()
|
||||
.syncIdEqualTo(track.syncId)
|
||||
.findFirstSync();
|
||||
if (!(service == null || chapterNumber <= (track.lastChapterRead ?? 0))) {
|
||||
if (track.status != TrackStatus.completed) {
|
||||
track.lastChapterRead = chapterNumber;
|
||||
if (track.lastChapterRead == track.totalChapter &&
|
||||
(track.totalChapter ?? 0) > 0) {
|
||||
track.status = TrackStatus.completed;
|
||||
track.finishedReadingDate = DateTime.now().millisecondsSinceEpoch;
|
||||
} else {
|
||||
track.status = manga.itemType == ItemType.manga
|
||||
? TrackStatus.reading
|
||||
: TrackStatus.watching;
|
||||
if (track.lastChapterRead == 1) {
|
||||
track.startedReadingDate = DateTime.now().millisecondsSinceEpoch;
|
||||
}
|
||||
}
|
||||
}
|
||||
ref
|
||||
.read(
|
||||
trackStateProvider(
|
||||
track: track,
|
||||
itemType: manga.itemType,
|
||||
widgetRef: ref,
|
||||
).notifier,
|
||||
)
|
||||
.updateManga();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
124
lib/utils/extensions/manga.dart
Normal file
124
lib/utils/extensions/manga.dart
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
import 'package:isar_community/isar.dart';
|
||||
import 'package:mangayomi/main.dart';
|
||||
import 'package:mangayomi/models/chapter.dart';
|
||||
import 'package:mangayomi/models/download.dart';
|
||||
import 'package:mangayomi/models/manga.dart';
|
||||
import 'package:mangayomi/models/settings.dart';
|
||||
|
||||
extension MangaExtensions on Manga {
|
||||
// ── For the READER: always ascending story order, filters applied ──────────
|
||||
List<Chapter> getChapterListForReading() {
|
||||
final settings = isar.settings.getSync(227)!;
|
||||
|
||||
final filterUnread =
|
||||
(settings.chapterFilterUnreadList!
|
||||
.where((e) => e.mangaId == id)
|
||||
.firstOrNull ??
|
||||
ChapterFilterUnread(mangaId: id, type: 0))
|
||||
.type!;
|
||||
|
||||
final filterBookmarked =
|
||||
(settings.chapterFilterBookmarkedList!
|
||||
.where((e) => e.mangaId == id)
|
||||
.firstOrNull ??
|
||||
ChapterFilterBookmarked(mangaId: id, type: 0))
|
||||
.type!;
|
||||
|
||||
final filterDownloaded =
|
||||
(settings.chapterFilterDownloadedList!
|
||||
.where((e) => e.mangaId == id)
|
||||
.firstOrNull ??
|
||||
ChapterFilterDownloaded(mangaId: id, type: 0))
|
||||
.type!;
|
||||
|
||||
final scanlators = settings.filterScanlatorList ?? [];
|
||||
final filter = scanlators.where((e) => e.mangaId == id).toList();
|
||||
final filterScanlator = filter.firstOrNull?.scanlators ?? [];
|
||||
|
||||
// Canonical ascending order (ch1 ... chN) — reader always moves forward.
|
||||
final data = chapters
|
||||
.toList(); // keep DB/insertion order, assumed ascending
|
||||
|
||||
final chapterIds = data.map((c) => c.id).whereType<int>().toList();
|
||||
final downloadedIds = (filterDownloaded == 0 || chapterIds.isEmpty)
|
||||
? const <int>{}
|
||||
: isar.downloads
|
||||
.filter()
|
||||
.anyOf(chapterIds, (q, id) => q.idEqualTo(id))
|
||||
.isDownloadEqualTo(true)
|
||||
.findAllSync()
|
||||
.map((d) => d.id!)
|
||||
.toSet();
|
||||
|
||||
return data
|
||||
.where(
|
||||
(e) => filterUnread == 1
|
||||
? e.isRead == false
|
||||
: filterUnread == 2
|
||||
? e.isRead == true
|
||||
: true,
|
||||
)
|
||||
.where(
|
||||
(e) => filterBookmarked == 1
|
||||
? e.isBookmarked == true
|
||||
: filterBookmarked == 2
|
||||
? e.isBookmarked == false
|
||||
: true,
|
||||
)
|
||||
.where((e) {
|
||||
if (filterDownloaded == 0) return true;
|
||||
final dl = downloadedIds.contains(e.id);
|
||||
return filterDownloaded == 1 ? dl : !dl;
|
||||
})
|
||||
.where((e) => !filterScanlator.contains(e.scanlator))
|
||||
.toList()
|
||||
.reversed
|
||||
.toList();
|
||||
}
|
||||
|
||||
// ── For the UI LIST: filters + user-chosen sort + reverse ─────────────────
|
||||
List<Chapter> getFilteredChapterList() {
|
||||
final settings = isar.settings.getSync(227)!;
|
||||
|
||||
final sortChapterEntry =
|
||||
settings.sortChapterList!.where((e) => e.mangaId == id).firstOrNull ??
|
||||
SortChapter(mangaId: id, index: 1, reverse: false);
|
||||
final sortIndex = sortChapterEntry.index!;
|
||||
final reverse = sortChapterEntry.reverse!;
|
||||
|
||||
// Start from the reading list so filter logic lives in one place.
|
||||
List<Chapter> list = getChapterListForReading();
|
||||
|
||||
switch (sortIndex) {
|
||||
case 0: // by scanlator, then date
|
||||
list.sort((a, b) {
|
||||
if (a.scanlator == null || b.scanlator == null) return 0;
|
||||
final s = a.scanlator!.compareTo(b.scanlator!);
|
||||
if (s != 0) return s;
|
||||
if (a.dateUpload == null || b.dateUpload == null) return 0;
|
||||
return (int.tryParse(a.dateUpload!) ?? 0).compareTo(
|
||||
int.tryParse(b.dateUpload!) ?? 0,
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 1: // by chapter number - reading list is already ascending
|
||||
break;
|
||||
case 2: // by upload date
|
||||
list.sort((a, b) {
|
||||
if (a.dateUpload == null || b.dateUpload == null) return 0;
|
||||
return (int.tryParse(a.dateUpload!) ?? 0).compareTo(
|
||||
int.tryParse(b.dateUpload!) ?? 0,
|
||||
);
|
||||
});
|
||||
break;
|
||||
case 3: // by name
|
||||
list.sort((a, b) {
|
||||
if (a.name == null || b.name == null) return 0;
|
||||
return a.name!.compareTo(b.name!);
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return reverse ? list.reversed.toList() : list;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue